diff --git a/.gitattributes b/.gitattributes
index db355084a6..6ab0760ad1 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,3 +4,8 @@
*.md text
*.html text
*.txt text
+*.yml text
+*.yaml text
+*.conf text
+*.sh text
+*.js text
\ No newline at end of file
diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
index 2b883490d2..eb588e25c9 100644
--- a/.github/workflows/coverage.yaml
+++ b/.github/workflows/coverage.yaml
@@ -29,6 +29,7 @@ jobs:
- name: Install Dependencies
run: |
sudo apt-get update
+ sudo apt-get install gettext
pip3 install invoke
invoke install
- name: Coverage Tests
@@ -42,6 +43,8 @@ jobs:
rm test_db.sqlite
invoke migrate
invoke import-records -f data.json
+ - name: Test Translations
+ run: invoke translate
- name: Check Migration Files
run: python3 ci/check_migration_files.py
- name: Upload Coverage Report
diff --git a/.github/workflows/docker_build.yaml b/.github/workflows/docker_build.yaml
index c9f8a69654..df747bc56e 100644
--- a/.github/workflows/docker_build.yaml
+++ b/.github/workflows/docker_build.yaml
@@ -1,8 +1,11 @@
-# Test that the docker file builds correctly
+# Build and push latest docker image on push to master branch
-name: Docker
+name: Docker Build
-on: ["push", "pull_request"]
+on:
+ push:
+ branches:
+ - 'master'
jobs:
@@ -10,9 +13,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Build Server Image
- run: cd docker/inventree && docker build . --tag inventree:$(date +%s)
- - name: Build nginx Image
- run: cd docker/nginx && docker build . --tag nxinx:$(date +%s)
-
\ No newline at end of file
+ - name: Checkout Code
+ uses: actions/checkout@v2
+ - name: Login to Dockerhub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+ - name: Build and Push
+ uses: docker/build-push-action@v2
+ with:
+ context: ./docker
+ push: true
+ repository: inventree/inventree
+ tags: inventree/inventree:latest
+ - name: Image Digest
+ run: echo ${{ steps.docker_build.outputs.digest }}
diff --git a/.github/workflows/docker_publish.yaml b/.github/workflows/docker_publish.yaml
index 6870754ad3..667b860b13 100644
--- a/.github/workflows/docker_publish.yaml
+++ b/.github/workflows/docker_publish.yaml
@@ -7,12 +7,15 @@ on:
types: [published]
jobs:
- server_image:
+ publish_image:
name: Push InvenTree web server image to dockerhub
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v2
+ - name: cd
+ run: |
+ cd docker
- name: Push to Docker Hub
uses: docker/build-push-action@v1
with:
@@ -20,19 +23,4 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
repository: inventree/inventree
tag_with_ref: true
- dockerfile: docker/inventree/Dockerfile
-
- nginx_image:
- name: Push InvenTree nginx image to dockerhub
- runs-on: ubuntu-latest
- steps:
- - name: Check out repo
- uses: actions/checkout@v2
- - name: Push to Docker Hub
- uses: docker/build-push-action@v1
- with:
- username: ${{ secrets.DOCKER_USERNAME }}
- password: ${{ secrets.DOCKER_PASSWORD }}
- repository: inventree/nginx
- tag_with_ref: true
- dockerfile: docker/nginx/Dockerfile
+ dockerfile: ./Dockerfile
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index b648ad00b9..eaa9e5574d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,6 +40,7 @@ docs/_build
# Local static and media file storage (only when running in development mode)
inventree_media
inventree_static
+static_i18n
# Local config file
config.yaml
diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index c2441590f5..6f6953ccb5 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -280,11 +280,25 @@ def MakeBarcode(object_name, object_pk, object_data={}, **kwargs):
json string of the supplied data plus some other data
"""
+ url = kwargs.get('url', False)
brief = kwargs.get('brief', True)
data = {}
- if brief:
+ if url:
+ request = object_data.get('request', None)
+ item_url = object_data.get('item_url', None)
+ absolute_url = None
+
+ if request and item_url:
+ absolute_url = request.build_absolute_uri(item_url)
+ # Return URL (No JSON)
+ return absolute_url
+
+ if item_url:
+ # Return URL (No JSON)
+ return item_url
+ elif brief:
data[object_name] = object_pk
else:
data['tool'] = 'InvenTree'
diff --git a/InvenTree/InvenTree/management/commands/prerender.py b/InvenTree/InvenTree/management/commands/prerender.py
new file mode 100644
index 0000000000..28f4b21f15
--- /dev/null
+++ b/InvenTree/InvenTree/management/commands/prerender.py
@@ -0,0 +1,61 @@
+"""
+Custom management command to prerender files
+"""
+
+from django.core.management.base import BaseCommand
+from django.conf import settings
+from django.template.loader import render_to_string
+from django.utils.module_loading import import_string
+from django.http.request import HttpRequest
+from django.utils.translation import override as lang_over
+
+import os
+
+
+def render_file(file_name, source, target, locales, ctx):
+ """ renders a file into all provided locales """
+ for locale in locales:
+ target_file = os.path.join(target, locale + '.' + file_name)
+ with open(target_file, 'w') as localised_file:
+ with lang_over(locale):
+ renderd = render_to_string(os.path.join(source, file_name), ctx)
+ localised_file.write(renderd)
+
+
+class Command(BaseCommand):
+ """
+ django command to prerender files
+ """
+
+ def handle(self, *args, **kwargs):
+ # static directories
+ LC_DIR = settings.LOCALE_PATHS[0]
+ SOURCE_DIR = settings.STATICFILES_I18_SRC
+ TARGET_DIR = settings.STATICFILES_I18_TRG
+
+ # ensure static directory exists
+ if not os.path.exists(TARGET_DIR):
+ os.makedirs(TARGET_DIR, exist_ok=True)
+
+ # collect locales
+ locales = {}
+ for locale in os.listdir(LC_DIR):
+ path = os.path.join(LC_DIR, locale)
+ if os.path.exists(path) and os.path.isdir(path):
+ locales[locale] = locale
+
+ # render!
+ request = HttpRequest()
+ ctx = {}
+ processors = tuple(import_string(path) for path in settings.STATFILES_I18_PROCESSORS)
+ for processor in processors:
+ ctx.update(processor(request))
+
+ for file in os.listdir(SOURCE_DIR, ):
+ path = os.path.join(SOURCE_DIR, file)
+ if os.path.exists(path) and os.path.isfile(path):
+ print(f"render {file}")
+ render_file(file, SOURCE_DIR, TARGET_DIR, locales, ctx)
+ else:
+ raise NotImplementedError('Using multi-level directories is not implemented at this point') # TODO multilevel dir if needed
+ print(f"rendered all files in {SOURCE_DIR}")
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 1b84f0c51a..4e9ed35748 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -52,6 +52,9 @@ def get_setting(environment_var, backup_val, default_value=None):
# Determine if we are running in "test" mode e.g. "manage.py test"
TESTING = 'test' in sys.argv
+# New requirement for django 3.2+
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -190,6 +193,17 @@ STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'InvenTree', 'static'),
]
+# Translated Template settings
+STATICFILES_I18_PREFIX = 'i18n'
+STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js')
+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)
+
+STATFILES_I18_PROCESSORS = [
+ 'InvenTree.context.status_codes',
+]
+
# Color Themes Directory
STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
@@ -396,7 +410,6 @@ for key in db_keys:
env_var = os.environ.get(env_key, None)
if env_var:
- logger.info(f"{env_key}={env_var}")
# Override configuration value
db_config[key] = env_var
diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css
index 153931f974..9d322f339d 100644
--- a/InvenTree/InvenTree/static/css/inventree.css
+++ b/InvenTree/InvenTree/static/css/inventree.css
@@ -185,6 +185,10 @@
color: #c55;
}
+.icon-orange {
+ color: #fcba03;
+}
+
.icon-green {
color: #43bb43;
}
diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py
index e9846e445a..5531d4c270 100644
--- a/InvenTree/InvenTree/status.py
+++ b/InvenTree/InvenTree/status.py
@@ -56,17 +56,26 @@ def is_email_configured():
configured = True
if not settings.EMAIL_HOST:
- logger.warning("EMAIL_HOST is not configured")
configured = False
+ # Display warning unless in test mode
+ if not settings.TESTING:
+ logger.warning("EMAIL_HOST is not configured")
+
if not settings.EMAIL_HOST_USER:
- logger.warning("EMAIL_HOST_USER is not configured")
configured = False
+
+ # Display warning unless in test mode
+ if not settings.TESTING:
+ logger.warning("EMAIL_HOST_USER is not configured")
if not settings.EMAIL_HOST_PASSWORD:
- logger.warning("EMAIL_HOST_PASSWORD is not configured")
configured = False
+ # Display warning unless in test mode
+ if not settings.TESTING:
+ logger.warning("EMAIL_HOST_PASSWORD is not configured")
+
return configured
diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py
index 5eb97504c6..6294eba06e 100644
--- a/InvenTree/InvenTree/status_codes.py
+++ b/InvenTree/InvenTree/status_codes.py
@@ -62,6 +62,14 @@ class StatusCode:
def items(cls):
return cls.options.items()
+ @classmethod
+ def keys(cls):
+ return cls.options.keys()
+
+ @classmethod
+ def labels(cls):
+ return cls.options.values()
+
@classmethod
def label(cls, value):
""" Return the status code label associated with the provided value """
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 88160e76c1..da7799397e 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -11,6 +11,7 @@ from django.contrib import admin
from django.contrib.auth import views as auth_views
from company.urls import company_urls
+from company.urls import manufacturer_part_urls
from company.urls import supplier_part_urls
from company.urls import price_break_urls
@@ -115,6 +116,7 @@ dynamic_javascript_urls = [
urlpatterns = [
url(r'^part/', include(part_urls)),
+ url(r'^manufacturer-part/', include(manufacturer_part_urls)),
url(r'^supplier-part/', include(supplier_part_urls)),
url(r'^price-break/', include(price_break_urls)),
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index b79323e1e7..361fec152a 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -8,7 +8,7 @@ import re
import common.models
-INVENTREE_SW_VERSION = "0.2.1 pre"
+INVENTREE_SW_VERSION = "0.2.2 pre"
# Increment this number whenever there is a significant change to the API that any clients need to know about
INVENTREE_API_VERSION = 2
@@ -19,6 +19,14 @@ def inventreeInstanceName():
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
+def inventreeInstanceTitle():
+ """ Returns the InstanceTitle for the current database """
+ if common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE_TITLE", False):
+ return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
+ else:
+ return 'InvenTree'
+
+
def inventreeVersion():
""" Returns the InvenTree version string """
return INVENTREE_SW_VERSION
diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py
index e6331f2b6a..10cc7e2024 100644
--- a/InvenTree/build/api.py
+++ b/InvenTree/build/api.py
@@ -11,7 +11,7 @@ from rest_framework import generics
from django.conf.urls import url, include
-from InvenTree.helpers import str2bool
+from InvenTree.helpers import str2bool, isNull
from InvenTree.status_codes import BuildStatus
from .models import Build, BuildItem
@@ -194,7 +194,11 @@ class BuildItemList(generics.ListCreateAPIView):
output = params.get('output', None)
if output:
- queryset = queryset.filter(install_into=output)
+
+ if isNull(output):
+ queryset = queryset.filter(install_into=None)
+ else:
+ queryset = queryset.filter(install_into=output)
return queryset
diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py
index 0726779b87..e60df22c21 100644
--- a/InvenTree/build/forms.py
+++ b/InvenTree/build/forms.py
@@ -3,6 +3,7 @@ Django Forms for interacting with Build objects
"""
# -*- coding: utf-8 -*-
+
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
@@ -12,6 +13,8 @@ from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField
from InvenTree.fields import DatePickerFormField
+from InvenTree.status_codes import StockStatus
+
from .models import Build, BuildItem, BuildOrderAttachment
from stock.models import StockLocation, StockItem
@@ -165,16 +168,10 @@ class AutoAllocateForm(HelperForm):
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Confirm stock allocation'))
- # Keep track of which build output we are interested in
- output = forms.ModelChoiceField(
- queryset=StockItem.objects.all(),
- )
-
class Meta:
model = Build
fields = [
'confirm',
- 'output',
]
@@ -214,6 +211,13 @@ class CompleteBuildOutputForm(HelperForm):
help_text=_('Location of completed parts'),
)
+ stock_status = forms.ChoiceField(
+ label=_('Status'),
+ help_text=_('Build output stock status'),
+ initial=StockStatus.OK,
+ choices=StockStatus.items(),
+ )
+
confirm_incomplete = forms.BooleanField(
required=False,
label=_('Confirm incomplete'),
@@ -232,10 +236,15 @@ class CompleteBuildOutputForm(HelperForm):
fields = [
'location',
'output',
+ 'stock_status',
'confirm',
'confirm_incomplete',
]
+ def __init__(self, *args, **kwargs):
+
+ super().__init__(*args, **kwargs)
+
class CancelBuildForm(HelperForm):
""" Form for cancelling a build """
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index 4ee8de0d73..16c0e5bb7f 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -22,7 +22,7 @@ from markdownx.models import MarkdownxField
from mptt.models import MPTTModel, TreeForeignKey
-from InvenTree.status_codes import BuildStatus
+from InvenTree.status_codes import BuildStatus, StockStatus
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
from InvenTree.validators import validate_build_order_reference
from InvenTree.models import InvenTreeAttachment
@@ -314,6 +314,42 @@ class Build(MPTTModel):
'sub_part'
)
+ @property
+ def tracked_bom_items(self):
+ """
+ Returns the "trackable" BOM items for this BuildOrder
+ """
+
+ items = self.bom_items
+ items = items.filter(sub_part__trackable=True)
+
+ return items
+
+ def has_tracked_bom_items(self):
+ """
+ Returns True if this BuildOrder has trackable BomItems
+ """
+
+ return self.tracked_bom_items.count() > 0
+
+ @property
+ def untracked_bom_items(self):
+ """
+ Returns the "non trackable" BOM items for this BuildOrder
+ """
+
+ items = self.bom_items
+ items = items.filter(sub_part__trackable=False)
+
+ return items
+
+ def has_untracked_bom_items(self):
+ """
+ Returns True if this BuildOrder has non trackable BomItems
+ """
+
+ return self.untracked_bom_items.count() > 0
+
@property
def remaining(self):
"""
@@ -449,6 +485,9 @@ class Build(MPTTModel):
if self.completed < self.quantity:
return False
+ if not self.areUntrackedPartsFullyAllocated():
+ return False
+
# No issues!
return True
@@ -458,7 +497,7 @@ class Build(MPTTModel):
Mark this build as complete
"""
- if not self.can_complete:
+ if self.incomplete_count > 0:
return
self.completion_date = datetime.now().date()
@@ -466,6 +505,9 @@ class Build(MPTTModel):
self.status = BuildStatus.COMPLETE
self.save()
+ # Remove untracked allocated stock
+ self.subtractUntrackedStock(user)
+
# Ensure that there are no longer any BuildItem objects
# which point to thie Build Order
self.allocated_stock.all().delete()
@@ -489,7 +531,7 @@ class Build(MPTTModel):
self.status = BuildStatus.CANCELLED
self.save()
- def getAutoAllocations(self, output):
+ def getAutoAllocations(self):
"""
Return a list of StockItem objects which will be allocated
using the 'AutoAllocate' function.
@@ -521,15 +563,19 @@ class Build(MPTTModel):
part = bom_item.sub_part
+ # If the part is "trackable" it cannot be auto-allocated
+ if part.trackable:
+ continue
+
# Skip any parts which are already fully allocated
- if self.isPartFullyAllocated(part, output):
+ if self.isPartFullyAllocated(part, None):
continue
# How many parts are required to complete the output?
- required = self.unallocatedQuantity(part, output)
+ required = self.unallocatedQuantity(part, None)
# Grab a list of stock items which are available
- stock_items = self.availableStockItems(part, output)
+ stock_items = self.availableStockItems(part, None)
# Ensure that the available stock items are in the correct location
if self.take_from is not None:
@@ -544,7 +590,6 @@ class Build(MPTTModel):
build_items = BuildItem.objects.filter(
build=self,
stock_item=stock_item,
- install_into=output
)
if len(build_items) > 0:
@@ -567,24 +612,45 @@ class Build(MPTTModel):
return allocations
@transaction.atomic
- def unallocateStock(self, output=None, part=None):
+ def unallocateOutput(self, output, part=None):
"""
- Deletes all stock allocations for this build.
-
- Args:
- output: Specify which build output to delete allocations (optional)
-
+ Unallocate all stock which are allocated against the provided "output" (StockItem)
"""
- allocations = BuildItem.objects.filter(build=self.pk)
-
- if output:
- allocations = allocations.filter(install_into=output.pk)
+ allocations = BuildItem.objects.filter(
+ build=self,
+ install_into=output
+ )
if part:
allocations = allocations.filter(stock_item__part=part)
- # Remove all the allocations
+ allocations.delete()
+
+ @transaction.atomic
+ def unallocateUntracked(self, part=None):
+ """
+ Unallocate all "untracked" stock
+ """
+
+ allocations = BuildItem.objects.filter(
+ build=self,
+ install_into=None
+ )
+
+ if part:
+ allocations = allocations.filter(stock_item__part=part)
+
+ allocations.delete()
+
+ @transaction.atomic
+ def unallocateAll(self):
+ """
+ Deletes all stock allocations for this build.
+ """
+
+ allocations = BuildItem.objects.filter(build=self)
+
allocations.delete()
@transaction.atomic
@@ -679,13 +745,13 @@ class Build(MPTTModel):
raise ValidationError(_("Build output does not match Build Order"))
# Unallocate all build items against the output
- self.unallocateStock(output)
+ self.unallocateOutput(output)
# Remove the build output from the database
output.delete()
@transaction.atomic
- def autoAllocate(self, output):
+ def autoAllocate(self):
"""
Run auto-allocation routine to allocate StockItems to this Build.
@@ -702,7 +768,7 @@ class Build(MPTTModel):
See: getAutoAllocations()
"""
- allocations = self.getAutoAllocations(output)
+ allocations = self.getAutoAllocations()
for item in allocations:
# Create a new allocation
@@ -710,11 +776,29 @@ class Build(MPTTModel):
build=self,
stock_item=item['stock_item'],
quantity=item['quantity'],
- install_into=output,
+ install_into=None
)
build_item.save()
+ @transaction.atomic
+ def subtractUntrackedStock(self, user):
+ """
+ Called when the Build is marked as "complete",
+ this function removes the allocated untracked items from stock.
+ """
+
+ items = self.allocated_stock.filter(
+ stock_item__part__trackable=False
+ )
+
+ # Remove stock
+ for item in items:
+ item.complete_allocation(user)
+
+ # Delete allocation
+ items.all().delete()
+
@transaction.atomic
def completeBuildOutput(self, output, user, **kwargs):
"""
@@ -726,6 +810,7 @@ class Build(MPTTModel):
# Select the location for the build output
location = kwargs.get('location', self.destination)
+ status = kwargs.get('status', StockStatus.OK)
# List the allocated BuildItem objects for the given output
allocated_items = output.items_to_install.all()
@@ -733,9 +818,7 @@ class Build(MPTTModel):
for build_item in allocated_items:
# TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete
- # TODO: Use celery / redis to offload the actual object deletion...
- # REF: https://www.botreetechnologies.com/blog/implementing-celery-using-django-for-background-task-processing
- # REF: https://code.tutsplus.com/tutorials/using-celery-with-django-for-background-task-processing--cms-28732
+ # TODO: Use the background worker process to handle this task!
# Complete the allocation of stock for that item
build_item.complete_allocation(user)
@@ -747,6 +830,7 @@ class Build(MPTTModel):
output.build = self
output.is_building = False
output.location = location
+ output.status = status
output.save()
@@ -779,7 +863,7 @@ class Build(MPTTModel):
if output:
quantity *= output.quantity
else:
- quantity *= self.remaining
+ quantity *= self.quantity
return quantity
@@ -807,7 +891,13 @@ class Build(MPTTModel):
allocations = self.allocatedItems(part, output)
- allocated = allocations.aggregate(q=Coalesce(Sum('quantity'), 0))
+ allocated = allocations.aggregate(
+ q=Coalesce(
+ Sum('quantity'),
+ 0,
+ output_field=models.DecimalField(),
+ )
+ )
return allocated['q']
@@ -828,19 +918,39 @@ class Build(MPTTModel):
return self.unallocatedQuantity(part, output) == 0
- def isFullyAllocated(self, output):
+ def isFullyAllocated(self, output, verbose=False):
"""
Returns True if the particular build output is fully allocated.
"""
- for bom_item in self.bom_items:
+ # If output is not specified, we are talking about "untracked" items
+ if output is None:
+ bom_items = self.untracked_bom_items
+ else:
+ bom_items = self.tracked_bom_items
+
+ fully_allocated = True
+
+ for bom_item in bom_items:
part = bom_item.sub_part
if not self.isPartFullyAllocated(part, output):
- return False
+ fully_allocated = False
+
+ if verbose:
+ print(f"Part {part} is not fully allocated for output {output}")
+ else:
+ break
# All parts must be fully allocated!
- return True
+ return fully_allocated
+
+ def areUntrackedPartsFullyAllocated(self):
+ """
+ Returns True if the un-tracked parts are fully allocated for this BuildOrder
+ """
+
+ return self.isFullyAllocated(None)
def allocatedParts(self, output):
"""
@@ -849,7 +959,13 @@ class Build(MPTTModel):
allocated = []
- for bom_item in self.bom_items:
+ # If output is not specified, we are talking about "untracked" items
+ if output is None:
+ bom_items = self.untracked_bom_items
+ else:
+ bom_items = self.tracked_bom_items
+
+ for bom_item in bom_items:
part = bom_item.sub_part
if self.isPartFullyAllocated(part, output):
@@ -864,7 +980,13 @@ class Build(MPTTModel):
unallocated = []
- for bom_item in self.bom_items:
+ # If output is not specified, we are talking about "untracked" items
+ if output is None:
+ bom_items = self.untracked_bom_items
+ else:
+ bom_items = self.tracked_bom_items
+
+ for bom_item in bom_items:
part = bom_item.sub_part
if not self.isPartFullyAllocated(part, output):
@@ -1014,10 +1136,12 @@ class BuildItem(models.Model):
errors = {}
- if not self.install_into:
- raise ValidationError(_('Build item must specify a build output'))
-
try:
+
+ # If the 'part' is trackable, then the 'install_into' field must be set!
+ if self.stock_item.part and self.stock_item.part.trackable and not self.install_into:
+ raise ValidationError(_('Build item must specify a build output, as master part is marked as trackable'))
+
# Allocated part must be in the BOM for the master part
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False):
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)]
diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html
index 6efde49308..dee90a26a0 100644
--- a/InvenTree/build/templates/build/allocate.html
+++ b/InvenTree/build/templates/build/allocate.html
@@ -4,7 +4,7 @@
{% load inventree_extras %}
{% block page_title %}
-InvenTree | Allocate Parts
+{% inventree_title %} | {% trans "Allocate Parts" %}
{% endblock %}
{% block menubar %}
@@ -12,48 +12,41 @@ InvenTree | Allocate Parts
{% endblock %}
{% block heading %}
-{% trans "Incomplete Build Ouputs" %}
+{% trans "Allocate Stock to Build" %}
{% endblock %}
{% block details %}
-{% if build.is_complete %}
-
- {% trans "Build order has been completed" %}
-
-{% else %}
+{% if build.has_untracked_bom_items %}
+{% if build.active %}
- {% if build.active %}
-
-
-
-{% if build.incomplete_outputs %}
-
- {% for item in build.incomplete_outputs %}
- {% include "build/allocation_card.html" with item=item %}
- {% endfor %}
+{% if build.areUntrackedPartsFullyAllocated %}
+
+ {% trans "Untracked stock has been fully allocated for this Build Order" %}
{% else %}
-
- {% trans "Create a new build output" %}
- {% trans "No incomplete build outputs remain." %}
- {% trans "Create a new build output using the button above" %}
+
+ {% trans "Untracked stock has not been fully allocated for this Build Order" %}
+
+{% endif %}
+{% endif %}
+
+{% else %}
+
+ {% trans "This Build Order does not have any associated untracked BOM items" %}
- {% trans "This Build Order is allocated to Sales Order" %} {{ build.sales_order }}
+ {% object_link 'so-detail' build.sales_order.id build.sales_order as link %}
+ {% blocktrans %}This Build Order is allocated to Sales Order {{link}}{% endblocktrans %}
{% endif %}
{% if build.parent %}
- {% trans "This Build Order is a child of Build Order" %} {{ build.parent }}
+ {% object_link 'build-detail' build.parent.id build.parent as link %}
+ {% blocktrans %}This Build Order is a child of Build Order {{link}}{% endblocktrans %}
{% endif %}
{% endblock %}
+{% block header_post_content %}
+{% if build.active %}
+{% if build.can_complete %}
+
+ {% trans "Build Order is ready to mark as completed" %}
+
+{% endif %}
+{% if build.incomplete_count > 0 %}
+
+ {% trans "Build Order cannot be completed as outstanding outputs remain" %}
+
+{% endif %}
+{% if build.completed < build.quantity %}
+
+ {% trans "Required build quantity has not yet been completed" %}
+
+{% endif %}
+{% if not build.areUntrackedPartsFullyAllocated %}
+
+ {% trans "Stock has not been fully allocated to this Build Order" %}
+
{{ build.target_date }}
{% if build.is_overdue %}
- {% trans "Overdue" %}
+ {% trans "Overdue" %}
{% endif %}
@@ -169,6 +201,13 @@ src="{% static 'img/blank_image.png' %}"
});
$("#build-complete").on('click', function() {
+
+ {% if build.incomplete_count > 0 %}
+ showAlertDialog(
+ '{% trans "Incomplete Outputs" %}',
+ '{% trans "Build Order cannot be completed as incomplete build outputs remain" %}'
+ );
+ {% else %}
launchModalForm(
"{% url 'build-complete' build.id %}",
{
@@ -176,6 +215,7 @@ src="{% static 'img/blank_image.png' %}"
submit_text: '{% trans "Complete Build" %}',
}
);
+ {% endif %}
});
$('#print-build-report').click(function() {
diff --git a/InvenTree/build/templates/build/build_output.html b/InvenTree/build/templates/build/build_output.html
index 9a077524d4..00d7c5d5d2 100644
--- a/InvenTree/build/templates/build/build_output.html
+++ b/InvenTree/build/templates/build/build_output.html
@@ -6,19 +6,68 @@
{% include "build/navbar.html" with tab='output' %}
{% endblock %}
-{% block heading %}
-{% trans "Build Outputs" %}
-{% endblock %}
+{% block content_panels %}
-{% block details %}
+{% if not build.is_complete %}
+
+
+
+ {% trans "Incomplete Build Outputs" %}
+
+
-{% include "stock_table.html" with read_only=True %}
+
+
+ {% if build.active %}
+
+ {% trans "Create New Output" %}
+
+ {% endif %}
+
+
+ {% if build.incomplete_outputs %}
+
+ {% for item in build.incomplete_outputs %}
+ {% include "build/allocation_card.html" with item=item tracked_items=build.has_tracked_bom_items %}
+ {% endfor %}
+
+ {% else %}
+
+ {% trans "Create a new build output" %}
+ {% trans "No incomplete build outputs remain." %}
+ {% trans "Create a new build output using the button above" %}
+
+ {% endif %}
+
+
+
+{% endif %}
+
+
+
+
+ {% trans "Completed Build Outputs" %}
+
+
+
+
+ {% include "stock_table.html" with read_only=True %}
+
{% include "hover_image.html" with image=build.part.image hover=True %}
+ {% if output.serialized %}
+ {{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }}
+ {% else %}
{% decimal output.quantity %} x {{ output.part.full_name }}
+ {% endif %}
diff --git a/InvenTree/build/templates/build/create_build_item.html b/InvenTree/build/templates/build/create_build_item.html
index 8f58e884d6..9ebf5bb389 100644
--- a/InvenTree/build/templates/build/create_build_item.html
+++ b/InvenTree/build/templates/build/create_build_item.html
@@ -8,15 +8,13 @@
{% if output %}
- {% trans "The allocated stock will be installed into the following build output:" %}
-
- {{ output }}
+ {% blocktrans %}The allocated stock will be installed into the following build output: {{output}}{% endblocktrans %}
{% endif %}
{% if no_stock %}
- {% trans "No stock available for" %} {{ part }}
+ {% blocktrans %}No stock available for {{part}}{% endblocktrans %}
{% endif %}
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html
index f710928ee7..fbed17bfa3 100644
--- a/InvenTree/build/templates/build/index.html
+++ b/InvenTree/build/templates/build/index.html
@@ -5,7 +5,7 @@
{% load i18n %}
{% block page_title %}
-InvenTree | {% trans "Build Orders" %}
+{% inventree_title %} | {% trans "Build Orders" %}
{% endblock %}
{% block content %}
diff --git a/InvenTree/build/templates/build/navbar.html b/InvenTree/build/templates/build/navbar.html
index 5e27010861..785e2764b0 100644
--- a/InvenTree/build/templates/build/navbar.html
+++ b/InvenTree/build/templates/build/navbar.html
@@ -17,17 +17,11 @@
{% if build.active %}
-
+ {% include "hover_image.html" with image=part.image %}
+ {{ part.full_name}}
+
+ {{ part.description }}
+
+{% endif %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/company/templates/company/manufacturer_part_delete.html b/InvenTree/company/templates/company/manufacturer_part_delete.html
new file mode 100644
index 0000000000..58ecdbf23b
--- /dev/null
+++ b/InvenTree/company/templates/company/manufacturer_part_delete.html
@@ -0,0 +1,46 @@
+{% extends "modal_delete_form.html" %}
+{% load i18n %}
+
+{% block pre_form_content %}
+
+ {% trans "Are you sure you want to delete the following Manufacturer Parts?" %}
+
+{% for part in parts %}
+
+{% endfor %}
+
+{% endblock %}
+
+{% block form_data %}
+
+{% for part in parts %}
+
+
+
+
+
+ {% include "hover_image.html" with image=part.part.image %}
+ {{ part.part.full_name }}
+
+
+ {% include "hover_image.html" with image=part.manufacturer.image %}
+ {{ part.manufacturer.name }}
+
+
+ {{ part.MPN }}
+
+
+
+{% if part.supplier_parts.all|length > 0 %}
+
+
{% blocktrans with count=part.supplier_parts.all|length %}There are {{count}} suppliers defined for this manufacturer part. If you delete it, the following supplier parts will also be deleted:{% endblocktrans %}
+
+ {% for spart in part.supplier_parts.all %}
+
{{ spart.supplier.name }} - {{ spart.SKU }}
+ {% endfor %}
+
+
+{% endif %}
+{% endfor %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/company/templates/company/manufacturer_part_detail.html b/InvenTree/company/templates/company/manufacturer_part_detail.html
new file mode 100644
index 0000000000..fcb4abbfef
--- /dev/null
+++ b/InvenTree/company/templates/company/manufacturer_part_detail.html
@@ -0,0 +1,38 @@
+{% extends "company/manufacturer_part_base.html" %}
+{% load static %}
+{% load i18n %}
+
+{% block menubar %}
+{% include "company/manufacturer_part_navbar.html" with tab='details' %}
+{% endblock %}
+
+{% block heading %}
+{% trans "Manufacturer Part Details" %}
+{% endblock %}
+
+
+{% block details %}
+
+
diff --git a/InvenTree/company/templates/company/part_navbar.html b/InvenTree/company/templates/company/supplier_part_navbar.html
similarity index 97%
rename from InvenTree/company/templates/company/part_navbar.html
rename to InvenTree/company/templates/company/supplier_part_navbar.html
index 4236e7b734..09c3bc0443 100644
--- a/InvenTree/company/templates/company/part_navbar.html
+++ b/InvenTree/company/templates/company/supplier_part_navbar.html
@@ -1,4 +1,5 @@
{% load i18n %}
+{% load inventree_extras %}
diff --git a/InvenTree/company/templates/company/supplier_part_orders.html b/InvenTree/company/templates/company/supplier_part_orders.html
index f01bfa68d1..d523bea894 100644
--- a/InvenTree/company/templates/company/supplier_part_orders.html
+++ b/InvenTree/company/templates/company/supplier_part_orders.html
@@ -3,7 +3,7 @@
{% load i18n %}
{% block menubar %}
-{% include "company/part_navbar.html" with tab='orders' %}
+{% include "company/supplier_part_navbar.html" with tab='orders' %}
{% endblock %}
{% block heading %}
diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html
index 2314b5cfcf..a674837650 100644
--- a/InvenTree/company/templates/company/supplier_part_pricing.html
+++ b/InvenTree/company/templates/company/supplier_part_pricing.html
@@ -4,7 +4,7 @@
{% load inventree_extras %}
{% block menubar %}
-{% include "company/part_navbar.html" with tab='pricing' %}
+{% include "company/supplier_part_navbar.html" with tab='pricing' %}
{% endblock %}
{% block heading %}
diff --git a/InvenTree/company/templates/company/supplier_part_stock.html b/InvenTree/company/templates/company/supplier_part_stock.html
index 524c508957..1187b95bca 100644
--- a/InvenTree/company/templates/company/supplier_part_stock.html
+++ b/InvenTree/company/templates/company/supplier_part_stock.html
@@ -3,7 +3,7 @@
{% load i18n %}
{% block menubar %}
-{% include "company/part_navbar.html" with tab='stock' %}
+{% include "company/supplier_part_navbar.html" with tab='stock' %}
{% endblock %}
{% block heading %}
diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py
index 219a8ac019..a65beb4dc2 100644
--- a/InvenTree/company/test_api.py
+++ b/InvenTree/company/test_api.py
@@ -27,7 +27,7 @@ class CompanyTest(InvenTreeAPITestCase):
def test_company_list(self):
url = reverse('api-company-list')
- # There should be two companies
+ # There should be three companies
response = self.get(url)
self.assertEqual(len(response.data), 3)
@@ -62,3 +62,90 @@ class CompanyTest(InvenTreeAPITestCase):
data = {'search': 'cup'}
response = self.get(url, data)
self.assertEqual(len(response.data), 2)
+
+
+class ManufacturerTest(InvenTreeAPITestCase):
+ """
+ Series of tests for the Manufacturer DRF API
+ """
+
+ fixtures = [
+ 'category',
+ 'part',
+ 'location',
+ 'company',
+ 'manufacturer_part',
+ ]
+
+ roles = [
+ 'part.add',
+ 'part.change',
+ ]
+
+ def test_manufacturer_part_list(self):
+ url = reverse('api-manufacturer-part-list')
+
+ # There should be three manufacturer parts
+ response = self.get(url)
+ self.assertEqual(len(response.data), 3)
+
+ # Create manufacturer part
+ data = {
+ 'part': 1,
+ 'manufacturer': 7,
+ 'MPN': 'MPN_TEST',
+ }
+ response = self.client.post(url, data, format='json')
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(response.data['MPN'], 'MPN_TEST')
+
+ # Filter by manufacturer
+ data = {'company': 7}
+ response = self.get(url, data)
+ self.assertEqual(len(response.data), 3)
+
+ # Filter by part
+ data = {'part': 5}
+ response = self.get(url, data)
+ self.assertEqual(len(response.data), 2)
+
+ def test_manufacturer_part_detail(self):
+ url = reverse('api-manufacturer-part-detail', kwargs={'pk': 1})
+
+ response = self.get(url)
+ self.assertEqual(response.data['MPN'], 'MPN123')
+
+ # Change the MPN
+ data = {
+ 'MPN': 'MPN-TEST-123',
+ }
+ response = self.client.patch(url, data, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['MPN'], 'MPN-TEST-123')
+
+ def test_manufacturer_part_search(self):
+ # Test search functionality in manufacturer list
+ url = reverse('api-manufacturer-part-list')
+ data = {'search': 'MPN'}
+ response = self.get(url, data)
+ self.assertEqual(len(response.data), 3)
+
+ def test_supplier_part_create(self):
+ url = reverse('api-supplier-part-list')
+
+ # Create supplier part
+ data = {
+ 'part': 1,
+ 'supplier': 1,
+ 'SKU': 'SKU_TEST',
+ 'manufacturer': 7,
+ 'MPN': 'PART_NUMBER',
+ }
+ response = self.client.post(url, data, format='json')
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ # Check manufacturer part
+ manufacturer_part_id = int(response.data['manufacturer_part']['pk'])
+ url = reverse('api-manufacturer-part-detail', kwargs={'pk': manufacturer_part_id})
+ response = self.get(url)
+ self.assertEqual(response.data['MPN'], 'PART_NUMBER')
diff --git a/InvenTree/company/test_migrations.py b/InvenTree/company/test_migrations.py
index 1a51a5a5b0..bf6e212f7a 100644
--- a/InvenTree/company/test_migrations.py
+++ b/InvenTree/company/test_migrations.py
@@ -79,7 +79,7 @@ class TestManufacturerField(MigratorTestCase):
part=part,
supplier=supplier,
SKU='SCREW.002',
- manufacturer_name='Zero Corp'
+ manufacturer_name='Zero Corp',
)
self.assertEqual(Company.objects.count(), 1)
@@ -107,6 +107,136 @@ class TestManufacturerField(MigratorTestCase):
self.assertEqual(part.manufacturer.name, 'ACME')
+class TestManufacturerPart(MigratorTestCase):
+ """
+ Tests for migration 0034-0037 which added and transitioned to the ManufacturerPart model
+ """
+
+ migrate_from = ('company', '0033_auto_20210410_1528')
+ migrate_to = ('company', '0037_supplierpart_update_3')
+
+ def prepare(self):
+ """
+ Prepare the database by adding some test data 'before' the change:
+
+ - Part object
+ - Company object (supplier)
+ - SupplierPart object
+ """
+
+ Part = self.old_state.apps.get_model('part', 'part')
+ Company = self.old_state.apps.get_model('company', 'company')
+ SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
+
+ # Create an initial part
+ part = Part.objects.create(
+ name='CAP CER 0.1UF 10V X5R 0402',
+ description='CAP CER 0.1UF 10V X5R 0402',
+ purchaseable=True,
+ level=0,
+ tree_id=0,
+ lft=0,
+ rght=0,
+ )
+
+ # Create a manufacturer
+ manufacturer = Company.objects.create(
+ name='Murata',
+ description='Makes capacitors',
+ is_manufacturer=True,
+ is_supplier=False,
+ is_customer=False,
+ )
+
+ # Create suppliers
+ supplier_1 = Company.objects.create(
+ name='Digi-Key',
+ description='A supplier of components',
+ is_manufacturer=False,
+ is_supplier=True,
+ is_customer=False,
+ )
+
+ supplier_2 = Company.objects.create(
+ name='Mouser',
+ description='We sell components',
+ is_manufacturer=False,
+ is_supplier=True,
+ is_customer=False,
+ )
+
+ # Add some SupplierPart objects
+ SupplierPart.objects.create(
+ part=part,
+ supplier=supplier_1,
+ SKU='DK-MUR-CAP-123456-ND',
+ manufacturer=manufacturer,
+ MPN='MUR-CAP-123456',
+ )
+
+ SupplierPart.objects.create(
+ part=part,
+ supplier=supplier_1,
+ SKU='DK-MUR-CAP-987654-ND',
+ manufacturer=manufacturer,
+ MPN='MUR-CAP-987654',
+ )
+
+ SupplierPart.objects.create(
+ part=part,
+ supplier=supplier_2,
+ SKU='CAP-CER-01UF',
+ manufacturer=manufacturer,
+ MPN='MUR-CAP-123456',
+ )
+
+ # No MPN
+ SupplierPart.objects.create(
+ part=part,
+ supplier=supplier_2,
+ SKU='CAP-CER-01UF-1',
+ manufacturer=manufacturer,
+ )
+
+ # No Manufacturer
+ SupplierPart.objects.create(
+ part=part,
+ supplier=supplier_2,
+ SKU='CAP-CER-01UF-2',
+ MPN='MUR-CAP-123456',
+ )
+
+ # No Manufacturer data
+ SupplierPart.objects.create(
+ part=part,
+ supplier=supplier_2,
+ SKU='CAP-CER-01UF-3',
+ )
+
+ def test_manufacturer_part_objects(self):
+ """
+ Test that the new companies have been created successfully
+ """
+
+ # Check on the SupplierPart objects
+ SupplierPart = self.new_state.apps.get_model('company', 'supplierpart')
+
+ supplier_parts = SupplierPart.objects.all()
+ self.assertEqual(supplier_parts.count(), 6)
+
+ supplier_parts = SupplierPart.objects.filter(supplier__name='Mouser')
+ self.assertEqual(supplier_parts.count(), 4)
+
+ # Check on the ManufacturerPart objects
+ ManufacturerPart = self.new_state.apps.get_model('company', 'manufacturerpart')
+
+ manufacturer_parts = ManufacturerPart.objects.all()
+ self.assertEqual(manufacturer_parts.count(), 4)
+
+ manufacturer_part = manufacturer_parts.first()
+ self.assertEqual(manufacturer_part.MPN, 'MUR-CAP-123456')
+
+
class TestCurrencyMigration(MigratorTestCase):
"""
Tests for upgrade from basic currency support to django-money
diff --git a/InvenTree/company/test_views.py b/InvenTree/company/test_views.py
index 0163e65c29..e6eb54e0bf 100644
--- a/InvenTree/company/test_views.py
+++ b/InvenTree/company/test_views.py
@@ -10,6 +10,7 @@ from django.urls import reverse
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
+from .models import ManufacturerPart
from .models import SupplierPart
@@ -20,6 +21,7 @@ class CompanyViewTestBase(TestCase):
'part',
'location',
'company',
+ 'manufacturer_part',
'supplier_part',
]
@@ -200,3 +202,105 @@ class CompanyViewTest(CompanyViewTestBase):
response = self.client.get(reverse('customer-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, 'Create new Customer')
+
+
+class ManufacturerPartViewTests(CompanyViewTestBase):
+ """
+ Tests for the ManufacturerPart views.
+ """
+
+ def test_manufacturer_part_create(self):
+ """
+ Test the ManufacturerPartCreate view.
+ """
+
+ url = reverse('manufacturer-part-create')
+
+ # First check that we can GET the form
+ response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ self.assertEqual(response.status_code, 200)
+
+ # How many manufaturer parts are already in the database?
+ n = ManufacturerPart.objects.all().count()
+
+ data = {
+ 'part': 1,
+ 'manufacturer': 6,
+ }
+
+ # MPN is required! (form should fail)
+ (response, errors) = self.post(url, data, valid=False)
+
+ self.assertIsNotNone(errors.get('MPN', None))
+
+ data['MPN'] = 'TEST-ME-123'
+
+ (response, errors) = self.post(url, data, valid=True)
+
+ # Check that the ManufacturerPart was created!
+ self.assertEqual(n + 1, ManufacturerPart.objects.all().count())
+
+ # Try to create duplicate ManufacturerPart
+ (response, errors) = self.post(url, data, valid=False)
+
+ self.assertIsNotNone(errors.get('__all__', None))
+
+ # Check that the ManufacturerPart count stayed the same
+ self.assertEqual(n + 1, ManufacturerPart.objects.all().count())
+
+ def test_supplier_part_create(self):
+ """
+ Test that the SupplierPartCreate view creates Manufacturer Part.
+ """
+
+ url = reverse('supplier-part-create')
+
+ # First check that we can GET the form
+ response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ self.assertEqual(response.status_code, 200)
+
+ # How many manufacturer parts are already in the database?
+ n = ManufacturerPart.objects.all().count()
+
+ data = {
+ 'part': 1,
+ 'supplier': 1,
+ 'SKU': 'SKU_TEST',
+ 'manufacturer': 6,
+ 'MPN': 'MPN_TEST',
+ }
+
+ (response, errors) = self.post(url, data, valid=True)
+
+ # Check that the ManufacturerPart was created!
+ self.assertEqual(n + 1, ManufacturerPart.objects.all().count())
+
+ def test_manufacturer_part_delete(self):
+ """
+ Test the ManufacturerPartDelete view
+ """
+
+ url = reverse('manufacturer-part-delete')
+
+ # Get form using 'part' argument
+ response = self.client.get(url, {'part': '2'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ self.assertEqual(response.status_code, 200)
+
+ # POST to delete manufacturer part
+ n = ManufacturerPart.objects.count()
+ m = SupplierPart.objects.count()
+
+ response = self.client.post(
+ url,
+ {
+ 'manufacturer-part-2': 'manufacturer-part-2',
+ 'confirm_delete': True
+ },
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+ self.assertEqual(response.status_code, 200)
+
+ # Check that the ManufacturerPart was deleted
+ self.assertEqual(n - 1, ManufacturerPart.objects.count())
+ # Check that the SupplierParts were deleted
+ self.assertEqual(m - 2, SupplierPart.objects.count())
diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py
index f7bcd4e0b6..cb366e8a6d 100644
--- a/InvenTree/company/tests.py
+++ b/InvenTree/company/tests.py
@@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError
import os
-from .models import Company, Contact, SupplierPart
+from .models import Company, Contact, ManufacturerPart, SupplierPart
from .models import rename_company_image
from part.models import Part
@@ -22,6 +22,7 @@ class CompanySimpleTest(TestCase):
'part',
'location',
'bom',
+ 'manufacturer_part',
'supplier_part',
'price_breaks',
]
@@ -74,10 +75,10 @@ class CompanySimpleTest(TestCase):
self.assertEqual(acme.supplied_part_count, 4)
self.assertTrue(appel.has_parts)
- self.assertEqual(appel.supplied_part_count, 2)
+ self.assertEqual(appel.supplied_part_count, 3)
self.assertTrue(zerg.has_parts)
- self.assertEqual(zerg.supplied_part_count, 1)
+ self.assertEqual(zerg.supplied_part_count, 2)
def test_price_breaks(self):
@@ -166,3 +167,53 @@ class ContactSimpleTest(TestCase):
# Remove the parent company
Company.objects.get(pk=self.c.pk).delete()
self.assertEqual(Contact.objects.count(), 0)
+
+
+class ManufacturerPartSimpleTest(TestCase):
+
+ fixtures = [
+ 'category',
+ 'company',
+ 'location',
+ 'part',
+ 'manufacturer_part',
+ ]
+
+ def setUp(self):
+ # Create a manufacturer part
+ self.part = Part.objects.get(pk=1)
+ manufacturer = Company.objects.get(pk=1)
+
+ self.mp = ManufacturerPart.create(
+ part=self.part,
+ manufacturer=manufacturer,
+ mpn='PART_NUMBER',
+ description='THIS IS A MANUFACTURER PART',
+ )
+
+ # Create a supplier part
+ supplier = Company.objects.get(pk=5)
+ supplier_part = SupplierPart.objects.create(
+ part=self.part,
+ supplier=supplier,
+ SKU='SKU_TEST',
+ )
+
+ kwargs = {
+ 'manufacturer': manufacturer.id,
+ 'MPN': 'MPN_TEST',
+ }
+ supplier_part.save(**kwargs)
+
+ def test_exists(self):
+ self.assertEqual(ManufacturerPart.objects.count(), 5)
+
+ # Check that manufacturer part was created from supplier part creation
+ manufacturer_parts = ManufacturerPart.objects.filter(manufacturer=1)
+ self.assertEqual(manufacturer_parts.count(), 2)
+
+ def test_delete(self):
+ # Remove a part
+ Part.objects.get(pk=self.part.id).delete()
+ # Check that ManufacturerPart was deleted
+ self.assertEqual(ManufacturerPart.objects.count(), 3)
diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py
index b5ad06019b..b87b0626ae 100644
--- a/InvenTree/company/urls.py
+++ b/InvenTree/company/urls.py
@@ -13,7 +13,8 @@ company_detail_urls = [
# url(r'orders/?', views.CompanyDetail.as_view(template_name='company/orders.html'), name='company-detail-orders'),
- url(r'^parts/', views.CompanyDetail.as_view(template_name='company/detail_part.html'), name='company-detail-parts'),
+ url(r'^supplier-parts/', views.CompanyDetail.as_view(template_name='company/detail_supplier_part.html'), name='company-detail-supplier-parts'),
+ url(r'^manufacturer-parts/', views.CompanyDetail.as_view(template_name='company/detail_manufacturer_part.html'), name='company-detail-manufacturer-parts'),
url(r'^stock/', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'),
url(r'^purchase-orders/', views.CompanyDetail.as_view(template_name='company/purchase_orders.html'), name='company-detail-purchase-orders'),
url(r'^assigned-stock/', views.CompanyDetail.as_view(template_name='company/assigned_stock.html'), name='company-detail-assigned-stock'),
@@ -52,9 +53,26 @@ price_break_urls = [
url(r'^(?P\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'),
]
+manufacturer_part_detail_urls = [
+ url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'),
+
+ url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'),
+
+ url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'),
+]
+
+manufacturer_part_urls = [
+ url(r'^new/?', views.ManufacturerPartCreate.as_view(), name='manufacturer-part-create'),
+
+ url(r'delete/', views.ManufacturerPartDelete.as_view(), name='manufacturer-part-delete'),
+
+ url(r'^(?P\d+)/', include(manufacturer_part_detail_urls)),
+]
+
supplier_part_detail_urls = [
url(r'^edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
+ url(r'^manufacturers/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_manufacturers.html'), name='supplier-part-manufacturers'),
url(r'^pricing/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_pricing.html'), name='supplier-part-pricing'),
url(r'^orders/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_orders.html'), name='supplier-part-orders'),
url(r'^stock/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_stock.html'), name='supplier-part-stock'),
diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py
index bb16ea4bb6..be7d326c36 100644
--- a/InvenTree/company/views.py
+++ b/InvenTree/company/views.py
@@ -24,6 +24,7 @@ from InvenTree.helpers import str2bool
from InvenTree.views import InvenTreeRoleMixin
from .models import Company
+from .models import ManufacturerPart
from .models import SupplierPart
from .models import SupplierPriceBreak
@@ -31,6 +32,7 @@ from part.models import Part
from .forms import EditCompanyForm
from .forms import CompanyImageForm
+from .forms import EditManufacturerPartForm
from .forms import EditSupplierPartForm
from .forms import EditPriceBreakForm
from .forms import CompanyImageDownloadForm
@@ -331,6 +333,177 @@ class CompanyDelete(AjaxDeleteView):
}
+class ManufacturerPartDetail(DetailView):
+ """ Detail view for ManufacturerPart """
+ model = ManufacturerPart
+ template_name = 'company/manufacturer_part_detail.html'
+ context_object_name = 'part'
+ queryset = ManufacturerPart.objects.all()
+ permission_required = 'purchase_order.view'
+
+ def get_context_data(self, **kwargs):
+ ctx = super().get_context_data(**kwargs)
+
+ return ctx
+
+
+class ManufacturerPartEdit(AjaxUpdateView):
+ """ Update view for editing ManufacturerPart """
+
+ model = ManufacturerPart
+ context_object_name = 'part'
+ form_class = EditManufacturerPartForm
+ ajax_template_name = 'modal_form.html'
+ ajax_form_title = _('Edit Manufacturer Part')
+
+
+class ManufacturerPartCreate(AjaxCreateView):
+ """ Create view for making new ManufacturerPart """
+
+ model = ManufacturerPart
+ form_class = EditManufacturerPartForm
+ ajax_template_name = 'company/manufacturer_part_create.html'
+ ajax_form_title = _('Create New Manufacturer Part')
+ context_object_name = 'part'
+
+ def get_context_data(self):
+ """
+ Supply context data to the form
+ """
+
+ ctx = super().get_context_data()
+
+ # Add 'part' object
+ form = self.get_form()
+
+ part = form['part'].value()
+
+ try:
+ part = Part.objects.get(pk=part)
+ except (ValueError, Part.DoesNotExist):
+ part = None
+
+ ctx['part'] = part
+
+ return ctx
+
+ def get_form(self):
+ """ Create Form instance to create a new ManufacturerPart object.
+ Hide some fields if they are not appropriate in context
+ """
+ form = super(AjaxCreateView, self).get_form()
+
+ if form.initial.get('part', None):
+ # Hide the part field
+ form.fields['part'].widget = HiddenInput()
+
+ return form
+
+ def get_initial(self):
+ """ Provide initial data for new ManufacturerPart:
+
+ - If 'manufacturer_id' provided, pre-fill manufacturer field
+ - If 'part_id' provided, pre-fill part field
+ """
+ initials = super(ManufacturerPartCreate, self).get_initial().copy()
+
+ manufacturer_id = self.get_param('manufacturer')
+ part_id = self.get_param('part')
+
+ if manufacturer_id:
+ try:
+ initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
+ except (ValueError, Company.DoesNotExist):
+ pass
+
+ if part_id:
+ try:
+ initials['part'] = Part.objects.get(pk=part_id)
+ except (ValueError, Part.DoesNotExist):
+ pass
+
+ return initials
+
+
+class ManufacturerPartDelete(AjaxDeleteView):
+ """ Delete view for removing a ManufacturerPart.
+
+ ManufacturerParts can be deleted using a variety of 'selectors'.
+
+ - ?part= -> Delete a single ManufacturerPart object
+ - ?parts=[] -> Delete a list of ManufacturerPart objects
+
+ """
+
+ success_url = '/manufacturer/'
+ ajax_template_name = 'company/manufacturer_part_delete.html'
+ ajax_form_title = _('Delete Manufacturer Part')
+
+ role_required = 'purchase_order.delete'
+
+ parts = []
+
+ def get_context_data(self):
+ ctx = {}
+
+ ctx['parts'] = self.parts
+
+ return ctx
+
+ def get_parts(self):
+ """ Determine which ManufacturerPart object(s) the user wishes to delete.
+ """
+
+ self.parts = []
+
+ # User passes a single ManufacturerPart ID
+ if 'part' in self.request.GET:
+ try:
+ self.parts.append(ManufacturerPart.objects.get(pk=self.request.GET.get('part')))
+ except (ValueError, ManufacturerPart.DoesNotExist):
+ pass
+
+ elif 'parts[]' in self.request.GET:
+
+ part_id_list = self.request.GET.getlist('parts[]')
+
+ self.parts = ManufacturerPart.objects.filter(id__in=part_id_list)
+
+ def get(self, request, *args, **kwargs):
+ self.request = request
+ self.get_parts()
+
+ return self.renderJsonResponse(request, form=self.get_form())
+
+ def post(self, request, *args, **kwargs):
+ """ Handle the POST action for deleting ManufacturerPart object.
+ """
+
+ self.request = request
+ self.parts = []
+
+ for item in self.request.POST:
+ if item.startswith('manufacturer-part-'):
+ pk = item.replace('manufacturer-part-', '')
+
+ try:
+ self.parts.append(ManufacturerPart.objects.get(pk=pk))
+ except (ValueError, ManufacturerPart.DoesNotExist):
+ pass
+
+ confirm = str2bool(self.request.POST.get('confirm_delete', False))
+
+ data = {
+ 'form_valid': confirm,
+ }
+
+ if confirm:
+ for part in self.parts:
+ part.delete()
+
+ return self.renderJsonResponse(self.request, data=data, form=self.get_form())
+
+
class SupplierPartDetail(DetailView):
""" Detail view for SupplierPart """
model = SupplierPart
@@ -354,11 +527,25 @@ class SupplierPartEdit(AjaxUpdateView):
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Supplier Part')
+ def save(self, supplier_part, form, **kwargs):
+ """ Process ManufacturerPart data """
+
+ manufacturer = form.cleaned_data.get('manufacturer', None)
+ MPN = form.cleaned_data.get('MPN', None)
+ kwargs = {'manufacturer': manufacturer,
+ 'MPN': MPN,
+ }
+ supplier_part.save(**kwargs)
+
def get_form(self):
form = super().get_form()
supplier_part = self.get_object()
+ # Hide Manufacturer fields
+ form.fields['manufacturer'].widget = HiddenInput()
+ form.fields['MPN'].widget = HiddenInput()
+
# It appears that hiding a MoneyField fails validation
# Therefore the idea to set the value before hiding
if form.is_valid():
@@ -368,6 +555,19 @@ class SupplierPartEdit(AjaxUpdateView):
return form
+ def get_initial(self):
+ """ Fetch data from ManufacturerPart """
+
+ initials = super(SupplierPartEdit, self).get_initial().copy()
+
+ supplier_part = self.get_object()
+
+ if supplier_part.manufacturer_part:
+ initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id
+ initials['MPN'] = supplier_part.manufacturer_part.MPN
+
+ return initials
+
class SupplierPartCreate(AjaxCreateView):
""" Create view for making new SupplierPart """
@@ -415,6 +615,14 @@ class SupplierPartCreate(AjaxCreateView):
# Save the supplier part object
supplier_part = super().save(form)
+ # Process manufacturer data
+ manufacturer = form.cleaned_data.get('manufacturer', None)
+ MPN = form.cleaned_data.get('MPN', None)
+ kwargs = {'manufacturer': manufacturer,
+ 'MPN': MPN,
+ }
+ supplier_part.save(**kwargs)
+
single_pricing = form.cleaned_data.get('single_pricing', None)
if single_pricing:
@@ -433,6 +641,12 @@ class SupplierPartCreate(AjaxCreateView):
# Hide the part field
form.fields['part'].widget = HiddenInput()
+ if form.initial.get('manufacturer', None):
+ # Hide the manufacturer field
+ form.fields['manufacturer'].widget = HiddenInput()
+ # Hide the MPN field
+ form.fields['MPN'].widget = HiddenInput()
+
return form
def get_initial(self):
@@ -446,6 +660,7 @@ class SupplierPartCreate(AjaxCreateView):
manufacturer_id = self.get_param('manufacturer')
supplier_id = self.get_param('supplier')
part_id = self.get_param('part')
+ manufacturer_part_id = self.get_param('manufacturer_part')
supplier = None
@@ -461,6 +676,16 @@ class SupplierPartCreate(AjaxCreateView):
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
except (ValueError, Company.DoesNotExist):
pass
+
+ if manufacturer_part_id:
+ try:
+ # Get ManufacturerPart instance information
+ manufacturer_part_obj = ManufacturerPart.objects.get(pk=manufacturer_part_id)
+ initials['part'] = Part.objects.get(pk=manufacturer_part_obj.part.id)
+ initials['manufacturer'] = manufacturer_part_obj.manufacturer.id
+ initials['MPN'] = manufacturer_part_obj.MPN
+ except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist):
+ pass
if part_id:
try:
@@ -493,7 +718,7 @@ class SupplierPartDelete(AjaxDeleteView):
"""
success_url = '/supplier/'
- ajax_template_name = 'company/partdelete.html'
+ ajax_template_name = 'company/supplier_part_delete.html'
ajax_form_title = _('Delete Supplier Part')
role_required = 'purchase_order.delete'
diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py
index 96850f4cb0..5c1b104670 100644
--- a/InvenTree/label/models.py
+++ b/InvenTree/label/models.py
@@ -253,10 +253,12 @@ class StockItemLabel(LabelTemplate):
'part': stock_item.part,
'name': stock_item.part.full_name,
'ipn': stock_item.part.IPN,
+ 'revision': stock_item.part.revision,
'quantity': normalize(stock_item.quantity),
'serial': stock_item.serial,
'uid': stock_item.uid,
'qr_data': stock_item.format_barcode(brief=True),
+ 'qr_url': stock_item.format_barcode(url=True, request=request),
'tests': stock_item.testResultMap()
}
diff --git a/InvenTree/locale/de/LC_MESSAGES/django.mo b/InvenTree/locale/de/LC_MESSAGES/django.mo
index aa43358545..0dd13dedf0 100644
Binary files a/InvenTree/locale/de/LC_MESSAGES/django.mo and b/InvenTree/locale/de/LC_MESSAGES/django.mo differ
diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po
index 345a5448bb..539f42aa8b 100644
--- a/InvenTree/locale/de/LC_MESSAGES/django.po
+++ b/InvenTree/locale/de/LC_MESSAGES/django.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"Project-Id-Version: inventree1\n"
+"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
"PO-Revision-Date: 2021-04-21 10:02\n"
@@ -11,11 +11,6 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Crowdin-Project: inventree1\n"
-"X-Crowdin-Project-ID: 450990\n"
-"X-Crowdin-Language: de\n"
-"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
-"X-Crowdin-File-ID: 98\n"
#: InvenTree/api.py:64
msgid "API endpoint not found"
@@ -7230,4 +7225,3 @@ msgstr "Berechtigungen Einträge zu ändern"
#: users/models.py:184
msgid "Permission to delete items"
msgstr "Berechtigung Einträge zu löschen"
-
diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po
index 7813b7d757..28684fc524 100644
--- a/InvenTree/locale/es/LC_MESSAGES/django.po
+++ b/InvenTree/locale/es/LC_MESSAGES/django.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"Project-Id-Version: inventree1\n"
+"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
"PO-Revision-Date: 2021-04-21 09:33\n"
@@ -11,11 +11,6 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Crowdin-Project: inventree1\n"
-"X-Crowdin-Project-ID: 450990\n"
-"X-Crowdin-Language: es-ES\n"
-"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
-"X-Crowdin-File-ID: 98\n"
#: InvenTree/api.py:64
msgid "API endpoint not found"
@@ -7226,4 +7221,3 @@ msgstr ""
#: users/models.py:184
msgid "Permission to delete items"
msgstr ""
-
diff --git a/InvenTree/locale/fr/LC_MESSAGES/django.mo b/InvenTree/locale/fr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000..2c90dd0c81
Binary files /dev/null and b/InvenTree/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/InvenTree/locale/fr/LC_MESSAGES/django.po b/InvenTree/locale/fr/LC_MESSAGES/django.po
index e29bb8c402..f344b4140c 100644
--- a/InvenTree/locale/fr/LC_MESSAGES/django.po
+++ b/InvenTree/locale/fr/LC_MESSAGES/django.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"Project-Id-Version: inventree1\n"
+"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
"PO-Revision-Date: 2021-04-21 09:33\n"
@@ -11,11 +11,6 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-"X-Crowdin-Project: inventree1\n"
-"X-Crowdin-Project-ID: 450990\n"
-"X-Crowdin-Language: fr\n"
-"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
-"X-Crowdin-File-ID: 98\n"
#: InvenTree/api.py:64
msgid "API endpoint not found"
diff --git a/InvenTree/locale/it/LC_MESSAGES/django.mo b/InvenTree/locale/it/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000..71cbdf3e9d
Binary files /dev/null and b/InvenTree/locale/it/LC_MESSAGES/django.mo differ
diff --git a/InvenTree/locale/it/LC_MESSAGES/django.po b/InvenTree/locale/it/LC_MESSAGES/django.po
index 0f7f0f6b69..978d9186a4 100644
--- a/InvenTree/locale/it/LC_MESSAGES/django.po
+++ b/InvenTree/locale/it/LC_MESSAGES/django.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"Project-Id-Version: inventree1\n"
+"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
"PO-Revision-Date: 2021-04-21 09:33\n"
@@ -11,11 +11,6 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Crowdin-Project: inventree1\n"
-"X-Crowdin-Project-ID: 450990\n"
-"X-Crowdin-Language: it\n"
-"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
-"X-Crowdin-File-ID: 98\n"
#: InvenTree/api.py:64
msgid "API endpoint not found"
diff --git a/InvenTree/locale/ja/LC_MESSAGES/django.mo b/InvenTree/locale/ja/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000..314bedb17d
Binary files /dev/null and b/InvenTree/locale/ja/LC_MESSAGES/django.mo differ
diff --git a/InvenTree/locale/ja/LC_MESSAGES/django.po b/InvenTree/locale/ja/LC_MESSAGES/django.po
index fccac0493e..14111c5c02 100644
--- a/InvenTree/locale/ja/LC_MESSAGES/django.po
+++ b/InvenTree/locale/ja/LC_MESSAGES/django.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"Project-Id-Version: inventree1\n"
+"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
"PO-Revision-Date: 2021-04-21 09:33\n"
@@ -11,11 +11,6 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-"X-Crowdin-Project: inventree1\n"
-"X-Crowdin-Project-ID: 450990\n"
-"X-Crowdin-Language: ja\n"
-"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
-"X-Crowdin-File-ID: 98\n"
#: InvenTree/api.py:64
msgid "API endpoint not found"
@@ -7226,4 +7221,3 @@ msgstr ""
#: users/models.py:184
msgid "Permission to delete items"
msgstr ""
-
diff --git a/InvenTree/locale/pl/LC_MESSAGES/django.mo b/InvenTree/locale/pl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000..16fcd00009
Binary files /dev/null and b/InvenTree/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/InvenTree/locale/pl/LC_MESSAGES/django.po b/InvenTree/locale/pl/LC_MESSAGES/django.po
index 2e69b2ba4b..8f82a13ba3 100644
--- a/InvenTree/locale/pl/LC_MESSAGES/django.po
+++ b/InvenTree/locale/pl/LC_MESSAGES/django.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"Project-Id-Version: inventree1\n"
+"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
"PO-Revision-Date: 2021-04-21 09:33\n"
@@ -11,11 +11,6 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
-"X-Crowdin-Project: inventree1\n"
-"X-Crowdin-Project-ID: 450990\n"
-"X-Crowdin-Language: pl\n"
-"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
-"X-Crowdin-File-ID: 98\n"
#: InvenTree/api.py:64
msgid "API endpoint not found"
@@ -7226,4 +7221,3 @@ msgstr ""
#: users/models.py:184
msgid "Permission to delete items"
msgstr ""
-
diff --git a/InvenTree/locale/ru/LC_MESSAGES/django.mo b/InvenTree/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000..7007a3d4e5
Binary files /dev/null and b/InvenTree/locale/ru/LC_MESSAGES/django.mo differ
diff --git a/InvenTree/locale/ru/LC_MESSAGES/django.po b/InvenTree/locale/ru/LC_MESSAGES/django.po
index 2791024852..e77638edc6 100644
--- a/InvenTree/locale/ru/LC_MESSAGES/django.po
+++ b/InvenTree/locale/ru/LC_MESSAGES/django.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"Project-Id-Version: inventree1\n"
+"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
"PO-Revision-Date: 2021-04-21 09:33\n"
@@ -11,11 +11,6 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
-"X-Crowdin-Project: inventree1\n"
-"X-Crowdin-Project-ID: 450990\n"
-"X-Crowdin-Language: ru\n"
-"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
-"X-Crowdin-File-ID: 98\n"
#: InvenTree/api.py:64
msgid "API endpoint not found"
@@ -7226,4 +7221,3 @@ msgstr ""
#: users/models.py:184
msgid "Permission to delete items"
msgstr ""
-
diff --git a/InvenTree/locale/zh/LC_MESSAGES/django.mo b/InvenTree/locale/zh/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000..6c5906d1cd
Binary files /dev/null and b/InvenTree/locale/zh/LC_MESSAGES/django.mo differ
diff --git a/InvenTree/locale/zh/LC_MESSAGES/django.po b/InvenTree/locale/zh/LC_MESSAGES/django.po
index 96bbb2f819..2534d27c42 100644
--- a/InvenTree/locale/zh/LC_MESSAGES/django.po
+++ b/InvenTree/locale/zh/LC_MESSAGES/django.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"Project-Id-Version: inventree1\n"
+"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-21 09:17+0000\n"
"PO-Revision-Date: 2021-04-21 09:33\n"
@@ -11,11 +11,6 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-"X-Crowdin-Project: inventree1\n"
-"X-Crowdin-Project-ID: 450990\n"
-"X-Crowdin-Language: zh-CN\n"
-"X-Crowdin-File: /l10_base/InvenTree/locale/en/LC_MESSAGES/django.po\n"
-"X-Crowdin-File-ID: 98\n"
#: InvenTree/api.py:64
msgid "API endpoint not found"
diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html
index 130ebd4590..5dfc30796f 100644
--- a/InvenTree/order/templates/order/order_base.html
+++ b/InvenTree/order/templates/order/order_base.html
@@ -6,7 +6,7 @@
{% load status_codes %}
{% block page_title %}
-InvenTree | {% trans "Purchase Order" %}
+{% inventree_title %} | {% trans "Purchase Order" %}
{% endblock %}
{% block thumbnail %}
diff --git a/InvenTree/order/templates/order/order_complete.html b/InvenTree/order/templates/order/order_complete.html
index 0f6aa55133..5c4ece7f1a 100644
--- a/InvenTree/order/templates/order/order_complete.html
+++ b/InvenTree/order/templates/order/order_complete.html
@@ -7,8 +7,8 @@
{% trans 'Mark this order as complete?' %}
{% if not order.is_complete %}
- {%trans 'This order has line items which have not been marked as received.
- Marking this order as complete will remove these line items.' %}
+ {% trans 'This order has line items which have not been marked as received.' %}
+ {% trans 'Marking this order as complete will remove these line items.' %}
{% if not part.order_supplier %}
- {% trans "Select a supplier for" %} {{ part.name }}
+ {% blocktrans with name=part.name %}Select a supplier for {{name}}{% endblocktrans %}
{% endif %}
{% if not supplier.selected_purchase_order %}
- {% trans "Select a purchase order for" %} {{ supplier.name }}
+ {% blocktrans with name=supplier.name %}Select a purchase order for {{name}}{% endblocktrans %}
{% endif %}
{% trans 'This category contains' %} {{ category.children.all|length }} {% trans 'child categories' %}.
+
{% blocktrans with count=category.children.all|length%}This category contains {{count}} child categories{% endblocktrans %}.
{% trans 'If this category is deleted, these child categories will be moved to the' %}
{% if category.parent %}
{{ category.parent.name }} {% trans 'category' %}.
@@ -22,9 +22,9 @@
{% endif %}
{% if category.parts.all|length > 0 %}
-
{% trans 'This category contains' %} {{ category.parts.all|length }} {% trans 'parts' %}.
+
{% blocktrans with count=category.parts.all|length %}This category contains {{count}} parts{% endblocktrans %}.
{% if category.parent %}
- {% trans 'If this category is deleted, these parts will be moved to the parent category' %} {{ category.parent.pathstring }}
+ {% blocktrans with path=category.parent.pathstring %}If this category is deleted, these parts will be moved to the parent category {{path}}{% endblocktrans %}
{% else %}
{% trans 'If this category is deleted, these parts will be moved to the top-level category Teile' %}
{% endif %}
diff --git a/InvenTree/part/templates/part/category_navbar.html b/InvenTree/part/templates/part/category_navbar.html
index 9374ecaaf1..e723db358d 100644
--- a/InvenTree/part/templates/part/category_navbar.html
+++ b/InvenTree/part/templates/part/category_navbar.html
@@ -8,17 +8,34 @@
+
+ {% endif %}
\ No newline at end of file
diff --git a/InvenTree/part/templates/part/copy_part.html b/InvenTree/part/templates/part/copy_part.html
index 1d0c04e2cd..1c9495bfc2 100644
--- a/InvenTree/part/templates/part/copy_part.html
+++ b/InvenTree/part/templates/part/copy_part.html
@@ -7,7 +7,7 @@
{% trans 'Duplicate Part' %}
- {% trans 'Make a copy of part' %} '{{ part.full_name }}'.
+ {% blocktrans with full_name=part.full_name %}Make a copy of part '{{full_name}}'.{% endblocktrans %}
{% if matches %}
diff --git a/InvenTree/part/templates/part/create_part.html b/InvenTree/part/templates/part/create_part.html
index d0e322d887..31ef1cb7e6 100644
--- a/InvenTree/part/templates/part/create_part.html
+++ b/InvenTree/part/templates/part/create_part.html
@@ -13,7 +13,8 @@
- Are you sure you want to delete part '{{ part.full_name }}'?
+ {% blocktrans with full_name=part.full_name %}Are you sure you want to delete part '{{full_name}}'?{% endblocktrans %}
{% if part.used_in_count %}
-
This part is used in BOMs for {{ part.used_in_count }} other parts. If you delete this part, the BOMs for the following parts will be updated:
+
{% blocktrans with count=part.used_in_count %}This part is used in BOMs for {{count}} other parts. If you delete this part, the BOMs for the following parts will be updated{% endblocktrans %}:
There are {{ part.stock_items.all|length }} stock entries defined for this part. If you delete this part, the following stock entries will also be deleted:
+
{% blocktrans with count=part.stock_items.all|length %}There are {{count}} stock entries defined for this part. If you delete this part, the following stock entries will also be deleted:{% endblocktrans %}
{% blocktrans with count=part.manufacturer_parts.all|length %}There are {{count}} manufacturers defined for this part. If you delete this part, the following manufacturer parts will also be deleted:{% endblocktrans %}
+
+ {% for spart in part.manufacturer_parts.all %}
+
There are {{ part.supplier_parts.all|length }} suppliers defined for this part. If you delete this part, the following supplier parts will also be deleted.
+
{% blocktrans with count=part.supplier_parts.all|length %}There are {{count}} suppliers defined for this part. If you delete this part, the following supplier parts will also be deleted:{% endblocktrans %}
There are {{ part.serials.all|length }} unique parts tracked for '{{ part.full_name }}'. Deleting this part will permanently remove this tracking information.
+
{% blocktrans with count=part.serials.all|length full_name=part.full_name %}There are {{count}} unique parts tracked for '{{full_name}}'. Deleting this part will permanently remove this tracking information.{% endblocktrans %}
{% endif %}
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/part/templates/part/stock.html b/InvenTree/part/templates/part/stock.html
index 2d1385f89c..067eda66a8 100644
--- a/InvenTree/part/templates/part/stock.html
+++ b/InvenTree/part/templates/part/stock.html
@@ -13,7 +13,7 @@
{% block details %}
{% if part.is_template %}
- {% trans 'Showing stock for all variants of' %} {{ part.full_name }}
+ {% blocktrans with full_name=part.full_name%}Showing stock for all variants of {{full_name}}{% endblocktrans %}
{% trans "Create new part variant" %}
- {% trans "Create a new variant of template" %} '{{ part.full_name }}'.
+ {% blocktrans with full_name=part.full_name %}Create a new variant of template '{{full_name}}'.{% endblocktrans %}
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py
index c0dc15c2b0..a92d95c766 100644
--- a/InvenTree/part/templatetags/inventree_extras.py
+++ b/InvenTree/part/templatetags/inventree_extras.py
@@ -4,6 +4,9 @@ over and above the built-in Django tags.
import os
from django import template
+from django.urls import reverse
+from django.utils.safestring import mark_safe
+from django.templatetags.static import StaticNode
from InvenTree import version, settings
import InvenTree.helpers
@@ -71,6 +74,12 @@ def inventree_instance_name(*args, **kwargs):
return version.inventreeInstanceName()
+@register.simple_tag()
+def inventree_title(*args, **kwargs):
+ """ Return the title for the current instance - respecting the settings """
+ return version.inventreeInstanceTitle()
+
+
@register.simple_tag()
def inventree_version(*args, **kwargs):
""" Return InvenTree version string """
@@ -164,3 +173,39 @@ def authorized_owners(group):
pass
return owners
+
+
+@register.simple_tag()
+def object_link(url_name, pk, ref):
+ """ Return highlighted link to object """
+
+ ref_url = reverse(url_name, kwargs={'pk': pk})
+ return mark_safe('{}'.format(ref_url, ref))
+
+
+class I18nStaticNode(StaticNode):
+ """
+ custom StaticNode
+ replaces a variable named *lng* in the path with the current language
+ """
+ def render(self, context):
+ self.path.var = self.path.var.format(lng=context.request.LANGUAGE_CODE)
+ ret = super().render(context)
+ return ret
+
+
+@register.tag('i18n_static')
+def do_i18n_static(parser, token):
+ """
+ Overrides normal static, adds language - lookup for prerenderd files #1485
+
+ usage (like static):
+ {% i18n_static path [as varname] %}
+ """
+ bits = token.split_contents()
+ loc_name = settings.STATICFILES_I18_PREFIX
+
+ # change path to called ressource
+ bits[1] = f"'{loc_name}/{{lng}}.{bits[1][1:-1]}'"
+ token.contents = ' '.join(bits)
+ return I18nStaticNode.handle_token(parser, token)
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index ed88e1dd55..4389003544 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -37,12 +37,54 @@ class PartAPITest(InvenTreeAPITestCase):
super().setUp()
def test_get_categories(self):
- """ Test that we can retrieve list of part categories """
+ """
+ Test that we can retrieve list of part categories,
+ with various filtering options.
+ """
+
url = reverse('api-part-category-list')
+
+ # Request *all* part categories
response = self.client.get(url, format='json')
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 8)
+ # Request top-level part categories only
+ response = self.client.get(
+ url,
+ {
+ 'parent': 'null',
+ },
+ format='json'
+ )
+
+ self.assertEqual(len(response.data), 2)
+
+ # Children of PartCategory<1>, cascade
+ response = self.client.get(
+ url,
+ {
+ 'parent': 1,
+ 'cascade': 'true',
+ },
+ format='json',
+ )
+
+ self.assertEqual(len(response.data), 5)
+
+ # Children of PartCategory<1>, do not cascade
+ response = self.client.get(
+ url,
+ {
+ 'parent': 1,
+ 'cascade': 'false',
+ },
+ format='json',
+ )
+
+ self.assertEqual(len(response.data), 3)
+
def test_add_categories(self):
""" Check that we can add categories """
data = {
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index f275edede2..b90b11b568 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -60,6 +60,7 @@ part_detail_urls = [
url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'),
+ url(r'^manufacturers/?', views.PartDetail.as_view(template_name='part/manufacturer.html'), name='part-manufacturers'),
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'),
url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'),
@@ -87,14 +88,26 @@ category_parameter_urls = [
url(r'^(?P\d+)/delete/', views.CategoryParameterTemplateDelete.as_view(), name='category-param-template-delete'),
]
-part_category_urls = [
- url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'),
- url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'),
+category_urls = [
- url(r'^parameters/', include(category_parameter_urls)),
+ # Create a new category
+ url(r'^new/', views.CategoryCreate.as_view(), name='category-create'),
- url(r'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'),
- url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
+ # Top level subcategory display
+ url(r'^subcategory/', views.PartIndex.as_view(template_name='part/subcategory.html'), name='category-index-subcategory'),
+
+ # Category detail views
+ url(r'(?P\d+)/', include([
+ url(r'^edit/', views.CategoryEdit.as_view(), name='category-edit'),
+ url(r'^delete/', views.CategoryDelete.as_view(), name='category-delete'),
+ url(r'^parameters/', include(category_parameter_urls)),
+
+ url(r'^subcategory/', views.CategoryDetail.as_view(template_name='part/subcategory.html'), name='category-subcategory'),
+ url(r'^parametric/', views.CategoryParametric.as_view(), name='category-parametric'),
+
+ # Anything else
+ url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
+ ]))
]
part_bom_urls = [
@@ -105,9 +118,6 @@ part_bom_urls = [
# URL list for part web interface
part_urls = [
- # Create a new category
- url(r'^category/new/?', views.CategoryCreate.as_view(), name='category-create'),
-
# Create a new part
url(r'^new/?', views.PartCreate.as_view(), name='part-create'),
@@ -124,7 +134,7 @@ part_urls = [
url(r'^(?P\d+)/', include(part_detail_urls)),
# Part category
- url(r'^category/(?P\d+)/', include(part_category_urls)),
+ url(r'^category/', include(category_urls)),
# Part related
url(r'^related-parts/', include(part_related_urls)),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 4efff3e9e3..229ae06109 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -1845,6 +1845,8 @@ class BomDownload(AjaxView):
supplier_data = str2bool(request.GET.get('supplier_data', False))
+ manufacturer_data = str2bool(request.GET.get('manufacturer_data', False))
+
levels = request.GET.get('levels', None)
if levels is not None:
@@ -1866,7 +1868,9 @@ class BomDownload(AjaxView):
max_levels=levels,
parameter_data=parameter_data,
stock_data=stock_data,
- supplier_data=supplier_data)
+ supplier_data=supplier_data,
+ manufacturer_data=manufacturer_data,
+ )
def get_data(self):
return {
@@ -1896,6 +1900,7 @@ class BomExport(AjaxView):
parameter_data = str2bool(request.POST.get('parameter_data', False))
stock_data = str2bool(request.POST.get('stock_data', False))
supplier_data = str2bool(request.POST.get('supplier_data', False))
+ manufacturer_data = str2bool(request.POST.get('manufacturer_data', False))
try:
part = Part.objects.get(pk=self.kwargs['pk'])
@@ -1913,6 +1918,7 @@ class BomExport(AjaxView):
url += '¶meter_data=' + str(parameter_data)
url += '&stock_data=' + str(stock_data)
url += '&supplier_data=' + str(supplier_data)
+ url += '&manufacturer_data=' + str(manufacturer_data)
if levels:
url += '&levels=' + str(levels)
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 7bad9df83e..0e64cecdbd 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -48,7 +48,7 @@ from rest_framework import generics, filters, permissions
class StockCategoryTree(TreeSerializer):
- title = 'Stock'
+ title = _('Stock')
model = StockLocation
@property
@@ -281,28 +281,46 @@ class StockLocationList(generics.ListCreateAPIView):
queryset = StockLocation.objects.all()
serializer_class = LocationSerializer
- def get_queryset(self):
+ def filter_queryset(self, queryset):
"""
Custom filtering:
- Allow filtering by "null" parent to retrieve top-level stock locations
"""
- queryset = super().get_queryset()
+ queryset = super().filter_queryset(queryset)
- loc_id = self.request.query_params.get('parent', None)
+ params = self.request.query_params
- if loc_id is not None:
+ loc_id = params.get('parent', None)
+
+ cascade = str2bool(params.get('cascade', False))
- # Look for top-level locations
- if isNull(loc_id):
+ # Do not filter by location
+ if loc_id is None:
+ pass
+ # Look for top-level locations
+ elif isNull(loc_id):
+
+ # If we allow "cascade" at the top-level, this essentially means *all* locations
+ if not cascade:
queryset = queryset.filter(parent=None)
-
- else:
- try:
- loc_id = int(loc_id)
- queryset = queryset.filter(parent=loc_id)
- except ValueError:
- pass
+
+ else:
+
+ try:
+ location = StockLocation.objects.get(pk=loc_id)
+
+ # All sub-locations to be returned too?
+ if cascade:
+ parents = location.get_descendants(include_self=True)
+ parent_ids = [p.id for p in parents]
+ queryset = queryset.filter(parent__in=parent_ids)
+
+ else:
+ queryset = queryset.filter(parent=location)
+
+ except (ValueError, StockLocation.DoesNotExist):
+ pass
return queryset
@@ -320,6 +338,11 @@ class StockLocationList(generics.ListCreateAPIView):
'description',
]
+ ordering_fields = [
+ 'name',
+ 'items',
+ ]
+
class StockList(generics.ListCreateAPIView):
""" API endpoint for list view of Stock objects
@@ -774,7 +797,7 @@ class StockList(generics.ListCreateAPIView):
company = params.get('company', None)
if company is not None:
- queryset = queryset.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer=company))
+ queryset = queryset.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer_part__manufacturer=company))
# Filter by supplier
supplier = params.get('supplier', None)
@@ -786,7 +809,7 @@ class StockList(generics.ListCreateAPIView):
manufacturer = params.get('manufacturer', None)
if manufacturer is not None:
- queryset = queryset.filter(supplier_part__manufacturer=manufacturer)
+ queryset = queryset.filter(supplier_part__manufacturer_part__manufacturer=manufacturer)
"""
Filter by the 'last updated' date of the stock item(s):
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index b57f96d0f7..7d9520a544 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -344,6 +344,8 @@ class StockItem(MPTTModel):
"stockitem",
self.id,
{
+ "request": kwargs.get('request', None),
+ "item_url": reverse('stock-item-detail', kwargs={'pk': self.id}),
"url": reverse('api-stock-detail', kwargs={'pk': self.id}),
},
**kwargs
@@ -632,6 +634,7 @@ class StockItem(MPTTModel):
self.customer = None
self.location = location
+ self.sales_order = None
self.save()
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index 9cb2538ba7..5b00c1dd17 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -84,7 +84,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
'sales_order',
'supplier_part',
'supplier_part__supplier',
- 'supplier_part__manufacturer',
+ 'supplier_part__manufacturer_part__manufacturer',
'allocations',
'sales_order_allocations',
'location',
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html
index 04518a0c65..bdd5c21fed 100644
--- a/InvenTree/stock/templates/stock/item_base.html
+++ b/InvenTree/stock/templates/stock/item_base.html
@@ -5,7 +5,7 @@
{% load i18n %}
{% block page_title %}
-InvenTree | {% trans "Stock Item" %} - {{ item }}
+{% inventree_title %} | {% trans "Stock Item" %} - {{ item }}
{% endblock %}
{% block sidenav %}
@@ -48,13 +48,17 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% for allocation in item.sales_order_allocations.all %}
- {% trans "This stock item is allocated to Sales Order" %} #{{ allocation.line.order }} ({% trans "Quantity" %}: {% decimal allocation.quantity %})
+ {% object_link 'so-detail' allocation.line.order.id allocation.line.order as link %}
+ {% decimal allocation.quantity as qty %}
+ {% blocktrans %}This stock item is allocated to Sales Order {{ link }} (Quantity: {{ qty }}){% endblocktrans %}
{% endfor %}
{% for allocation in item.allocations.all %}
- {% trans "This stock item is allocated to Build" %} #{{ allocation.build }} ({% trans "Quantity" %}: {% decimal allocation.quantity %})
+ {% object_link 'build-detail' allocation.build.id allocation.build %}
+ {% decimal allocation.quantity as qty %}
+ {% blocktrans %}This stock item is allocated to Build {{ link }} (Quantity: {{ qty }}){% endblocktrans %}
{% trans "Are you sure you want to delete this stock item?" %}
-This will remove {% decimal item.quantity %} units of {{ item.part.full_name }} from stock.
+{% decimal item.quantity as qty %}
+{% blocktrans with full_name=item.part.full_name %}This will remove {{qty}} units of {{full_name}} from stock.{% endblocktrans %}
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html
index 74e43f88bb..396a433566 100644
--- a/InvenTree/stock/templates/stock/location.html
+++ b/InvenTree/stock/templates/stock/location.html
@@ -2,8 +2,15 @@
{% load static %}
{% load inventree_extras %}
{% load i18n %}
+
+{% block menubar %}
+{% include "stock/location_navbar.html" with tab="stock" %}
+{% endblock %}
+
{% block content %}
+
+
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
{% if owner_control.value == "True" %}
{% authorized_owners location.owner as owners %}
@@ -120,36 +127,29 @@
+
-{% if location and location.children.all|length > 0 %}
-{% include 'stock/location_list.html' with children=location.children.all collapse_id="locations" %}
-{% elif locations|length > 0 %}
-{% include 'stock/location_list.html' with children=locations collapse_id="locations" %}
-{% endif %}
+{% block location_content %}
-
-
-{% include "stock_table.html" %}
+
\ No newline at end of file
diff --git a/InvenTree/stock/templates/stock/stock_app_base.html b/InvenTree/stock/templates/stock/stock_app_base.html
index dfe32c767d..209aea3956 100644
--- a/InvenTree/stock/templates/stock/stock_app_base.html
+++ b/InvenTree/stock/templates/stock/stock_app_base.html
@@ -1,12 +1,13 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
+{% load inventree_extras %}
{% block page_title %}
{% if location %}
-InvenTree | {% trans "Stock Location" %} - {{ location }}
+{% inventree_title %} | {% trans "Stock Location" %} - {{ location }}
{% else %}
-InvenTree | {% trans "Stock" %}
+{% inventree_title %} | {% trans "Stock" %}
{% endif %}
{% endblock %}
diff --git a/InvenTree/stock/templates/stock/stockitem_convert.html b/InvenTree/stock/templates/stock/stockitem_convert.html
index 55b565a60b..9d0b1f77b8 100644
--- a/InvenTree/stock/templates/stock/stockitem_convert.html
+++ b/InvenTree/stock/templates/stock/stockitem_convert.html
@@ -5,7 +5,7 @@
{% trans "Convert Stock Item" %}
- {% trans "This stock item is current an instance of " %}{{ item.part }}
+ {% blocktrans with part=item.part %}This stock item is current an instance of {{part}}{% endblocktrans %}
{% trans "It can be converted to one of the part variants listed below." %}
+{% endblock %}
{% endblock %}
diff --git a/InvenTree/templates/yesnolabel.html b/InvenTree/templates/yesnolabel.html
index cdc6070560..2117d42faa 100644
--- a/InvenTree/templates/yesnolabel.html
+++ b/InvenTree/templates/yesnolabel.html
@@ -1,5 +1,7 @@
+{% load i18n %}
+
{% if value %}
-Yes
+{% trans 'Yes' %}
{% else %}
-No
+{% trans 'No' %}
{% endif %}
\ No newline at end of file
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index e454281b37..73388a88bc 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -57,6 +57,7 @@ class RuleSet(models.Model):
'auth_user',
'auth_permission',
'authtoken_token',
+ 'authtoken_tokenproxy',
'users_ruleset',
],
'part_category': [
@@ -74,6 +75,7 @@ class RuleSet(models.Model):
'part_partrelated',
'part_partstar',
'company_supplierpart',
+ 'company_manufacturerpart',
],
'stock_location': [
'stock_stocklocation',
@@ -198,7 +200,8 @@ class RuleSet(models.Model):
if check_user_role(user, role, permission):
return True
- print("failed permission check for", table, permission)
+ # Print message instead of throwing an error
+ print("Failed permission check for", table, permission)
return False
@staticmethod
diff --git a/README.md b/README.md
index e841b713b7..aacc6ff670 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,29 @@
-[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
-[![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/r/inventree/inventree)
-[![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
-![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg)
-![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker_build.yaml/badge.svg)
-![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
-![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)
-![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg)
# InvenTree
+
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
+![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg)
+![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
+![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)
+![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg)
+
+
InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications.
InvenTree is designed to be lightweight and easy to use for SME or hobbyist applications, where many existing stock management solutions are bloated and cumbersome to use. Updating stock is a single-action process and does not require a complex system of work orders or stock transactions.
However, powerful business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information.
+# Docker
+
+[![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/r/inventree/inventree)
+![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker_build.yaml/badge.svg)
+
+InvenTree is [available via Docker](https://hub.docker.com/r/inventree/inventree). Read the [docker guide](https://inventree.readthedocs.io/en/latest/start/docker/) for full details.
+
# Companion App
InvenTree is supported by a [companion mobile app](https://inventree.readthedocs.io/en/latest/app/app/) which allows users access to stock control information and functionality.
@@ -24,6 +32,19 @@ InvenTree is supported by a [companion mobile app](https://inventree.readthedocs
*Currently the mobile app is only availble for Android*
+# Translation
+
+![de translation](https://img.shields.io/badge/dynamic/json?color=blue&label=de&style=flat&query=%24.progress.0.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
+![fr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&query=%24.progress.3.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
+![it translation](https://img.shields.io/badge/dynamic/json?color=blue&label=it&style=flat&query=%24.progress.4.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
+![pl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pl&style=flat&query=%24.progress.5.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
+![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&query=%24.progress.6.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
+![zh-CN translation](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-CN&style=flat&query=%24.progress.7.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-14720186-452300.json)
+
+Native language translation of the InvenTree web application is [community contributed via crowdin](https://crowdin.com/project/inventree). **Contributions are welcomed and encouraged**.
+
+To contribute to the translation effort, navigate to the [InvenTree crowdin project]([https://crowdin.com/project/inventree), create a free account, and start making translations suggestions for your language of choice!
+
# Documentation
For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/).
diff --git a/crowdin.yml b/crowdin.yml
new file mode 100644
index 0000000000..0fa92db24d
--- /dev/null
+++ b/crowdin.yml
@@ -0,0 +1,3 @@
+files:
+ - source: /InvenTree/locale/en/LC_MESSAGES/django.po
+ translation: /InvenTree/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%
diff --git a/docker/inventree/Dockerfile b/docker/Dockerfile
similarity index 72%
rename from docker/inventree/Dockerfile
rename to docker/Dockerfile
index fcba19e964..9682665e04 100644
--- a/docker/inventree/Dockerfile
+++ b/docker/Dockerfile
@@ -26,13 +26,18 @@ ENV INVENTREE_BACKUP_DIR="${INVENTREE_DATA_DIR}/backup"
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
+# Default web server port is 8000
+ENV INVENTREE_WEB_PORT="8000"
+
# Pass DB configuration through as environment variables
-ENV INVENTREE_DB_ENGINE="${INVENTREE_DB_ENGINE}"
-ENV INVENTREE_DB_NAME="${INVENTREE_DB_NAME}"
-ENV INVENTREE_DB_HOST="${INVENTREE_DB_HOST}"
-ENV INVENTREE_DB_PORT="${INVENTREE_DB_PORT}"
-ENV INVENTREE_DB_USER="${INVENTREE_DB_USER}"
-ENV INVENTREE_DB_PASSWORD="${INVENTREE_DB_PASSWORD}"
+# Default configuration = postgresql
+ENV INVENTREE_DB_ENGINE="postgresql"
+ENV INVENTREE_DB_NAME="inventree"
+ENV INVENTREE_DB_HOST="db"
+ENV INVENTREE_DB_PORT="5432"
+
+# INVENTREE_DB_USER must be specified at run-time
+# INVENTREE_DB_PASSWORD must be specified at run-time
LABEL org.label-schema.schema-version="1.0" \
org.label-schema.build-date=${DATE} \
@@ -56,14 +61,22 @@ RUN apk add --no-cache git make bash \
libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev \
libffi libffi-dev \
zlib zlib-dev
+
+# Cairo deps for WeasyPrint (these will be deprecated once WeasyPrint drops cairo requirement)
RUN apk add --no-cache cairo cairo-dev pango pango-dev
RUN apk add --no-cache fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans ttf-ubuntu-font-family font-croscore font-noto
-RUN apk add --no-cache python3
-RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
-RUN apk add --no-cache mariadb-connector-c mariadb-dev
-# Create required directories
-#RUN mkdir ${INVENTREE_DATA_DIR}}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
+# Python
+RUN apk add --no-cache python3 python3-dev
+
+# SQLite support
+RUN apk add --no-cache sqlite
+
+# PostgreSQL support
+RUN apk add --no-cache postgresql postgresql-contrib postgresql-dev libpq
+
+# MySQL support
+RUN apk add --no-cache mariadb-connector-c mariadb-dev mariadb-client
# Install required python packages
RUN pip install --upgrade pip setuptools wheel
@@ -82,14 +95,16 @@ RUN pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
COPY gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
# Copy startup scripts
-COPY start_server.sh ${INVENTREE_SRC_DIR}/start_server.sh
+COPY start_prod_server.sh ${INVENTREE_SRC_DIR}/start_prod_server.sh
+COPY start_dev_server.sh ${INVENTREE_SRC_DIR}/start_dev_server.sh
COPY start_worker.sh ${INVENTREE_SRC_DIR}/start_worker.sh
-RUN chmod 755 ${INVENTREE_SRC_DIR}/start_server.sh
+RUN chmod 755 ${INVENTREE_SRC_DIR}/start_prod_server.sh
+RUN chmod 755 ${INVENTREE_SRC_DIR}/start_dev_server.sh
RUN chmod 755 ${INVENTREE_SRC_DIR}/start_worker.sh
# exec commands should be executed from the "src" directory
WORKDIR ${INVENTREE_SRC_DIR}
# Let us begin
-CMD ["bash", "./start_server.sh"]
+CMD ["bash", "./start_prod_server.sh"]
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 90b5fa2668..e48b22d4b7 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -2,9 +2,9 @@ version: "3.8"
# Docker compose recipe for InvenTree
# - Runs PostgreSQL as the database backend
-# - Runs Gunicorn as the web server
+# - Runs Gunicorn as the InvenTree web server
+# - Runs the InvenTree background worker process
# - Runs nginx as a reverse proxy
-# - Runs the background worker process
# ---------------------------------
# IMPORTANT - READ BEFORE STARTING!
@@ -12,6 +12,7 @@ version: "3.8"
# Before running, ensure that you change the "/path/to/data" directory,
# specified in the "volumes" section at the end of this file.
# This path determines where the InvenTree data will be stored!
+#
services:
# Database service
@@ -25,6 +26,7 @@ services:
- 5432/tcp
environment:
- PGDATA=/var/lib/postgresql/data/pgdb
+ # The pguser and pgpassword values must be the same in the other containers
- POSTGRES_USER=pguser
- POSTGRES_PASSWORD=pgpassword
volumes:
@@ -34,39 +36,23 @@ services:
# InvenTree web server services
# Uses gunicorn as the web server
inventree:
- container_name: server
+ container_name: inventree
image: inventree/inventree:latest
expose:
- - 8080
+ - 8000
depends_on:
- db
volumes:
- data:/home/inventree/data
- static:/home/inventree/static
environment:
- - INVENTREE_DB_ENGINE=postgresql
- - INVENTREE_DB_NAME=inventree
+ # Default environment variables are configured to match the 'db' container
+ # Database permissions
- INVENTREE_DB_USER=pguser
- INVENTREE_DB_PASSWORD=pgpassword
- - INVENTREE_DB_PORT=5432
- - INVENTREE_DB_HOST=db
restart: unless-stopped
- # nginx acts as a reverse proxy
- # static files are served by nginx
- # web requests are redirected to gunicorn
- nginx:
- container_name: nginx
- image: inventree/nginx:latest
- depends_on:
- - inventree
- ports:
- # Change "1337" to the port where you want InvenTree web server to be available
- - 1337:80
- volumes:
- - static:/home/inventree/static
-
- # background worker process handles long-running or periodic tasks
+ # Background worker process handles long-running or periodic tasks
worker:
container_name: worker
image: inventree/inventree:latest
@@ -78,18 +64,34 @@ services:
- data:/home/inventree/data
- static:/home/inventree/static
environment:
- - INVENTREE_DB_ENGINE=postgresql
- - INVENTREE_DB_NAME=inventree
+ # Default environment variables are configured to match the 'inventree' container
- INVENTREE_DB_USER=pguser
- INVENTREE_DB_PASSWORD=pgpassword
- - INVENTREE_DB_PORT=5432
- - INVENTREE_DB_HOST=db
+ restart: unless-stopped
+
+ # nginx acts as a reverse proxy
+ # static files are served by nginx
+ # web requests are redirected to gunicorn
+ # NOTE: You will need to provide a working nginx.conf file!
+ proxy:
+ container_name: proxy
+ image: nginx
+ depends_on:
+ - inventree
+ ports:
+ # Change "1337" to the port that you want InvenTree web server to be available on
+ - 1337:80
+ volumes:
+ # Provide nginx.conf file to the container
+ # Refer to the provided example file as a starting point
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
+ # Static data volume is mounted to /var/www/static
+ - static:/var/www/static:ro
restart: unless-stopped
volumes:
- # Static files, shared between containers
- static:
- # Persistent data, stored externally
+ # NOTE: Change /path/to/data to a directory on your local machine
+ # Persistent data, stored external to the container(s)
data:
driver: local
driver_opts:
@@ -98,3 +100,5 @@ volumes:
# This directory specified where InvenTree data are stored "outside" the docker containers
# Change this path to a local system path where you want InvenTree data stored
device: /path/to/data
+ # Static files, shared between containers
+ static:
\ No newline at end of file
diff --git a/docker/inventree/gunicorn.conf.py b/docker/gunicorn.conf.py
similarity index 100%
rename from docker/inventree/gunicorn.conf.py
rename to docker/gunicorn.conf.py
diff --git a/docker/nginx.conf b/docker/nginx.conf
new file mode 100644
index 0000000000..ace56165aa
--- /dev/null
+++ b/docker/nginx.conf
@@ -0,0 +1,32 @@
+server {
+
+ # Listen for connection on (internal) port 80
+ listen 80;
+
+ location / {
+ # Change 'inventree' to the name of the inventree server container,
+ # and '8000' to the INVENTREE_WEB_PORT (if not default)
+ proxy_pass http://inventree:8000;
+
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $host;
+
+ proxy_redirect off;
+
+ client_max_body_size 100M;
+
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ }
+
+ # Redirect any requests for static files
+ location /static/ {
+ alias /var/www/static/;
+ autoindex on;
+ }
+
+}
\ No newline at end of file
diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile
deleted file mode 100644
index e754597f02..0000000000
--- a/docker/nginx/Dockerfile
+++ /dev/null
@@ -1,14 +0,0 @@
-FROM nginx:1.19.0-alpine
-
-# Create user account
-RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
-
-ENV HOME=/home/inventree
-WORKDIR $HOME
-
-# Create the "static" volume directory
-RUN mkdir $HOME/static
-
-RUN rm /etc/nginx/conf.d/default.conf
-COPY nginx.conf /etc/nginx/conf.d
-
diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf
deleted file mode 100644
index 0f25f51674..0000000000
--- a/docker/nginx/nginx.conf
+++ /dev/null
@@ -1,21 +0,0 @@
-upstream inventree {
- server inventree:8080;
-}
-
-server {
-
- listen 80;
-
- location / {
- proxy_pass http://inventree;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header Host $host;
- proxy_redirect off;
- client_max_body_size 100M;
- }
-
- location /static/ {
- alias /home/inventree/static/;
- }
-
-}
\ No newline at end of file
diff --git a/docker/start_dev_server.sh b/docker/start_dev_server.sh
new file mode 100644
index 0000000000..703d577ed5
--- /dev/null
+++ b/docker/start_dev_server.sh
@@ -0,0 +1,46 @@
+#!/bin/sh
+
+# Create required directory structure (if it does not already exist)
+if [[ ! -d "$INVENTREE_STATIC_ROOT" ]]; then
+ echo "Creating directory $INVENTREE_STATIC_ROOT"
+ mkdir $INVENTREE_STATIC_ROOT
+fi
+
+if [[ ! -d "$INVENTREE_MEDIA_ROOT" ]]; then
+ echo "Creating directory $INVENTREE_MEDIA_ROOT"
+ mkdir $INVENTREE_MEDIA_ROOT
+fi
+
+if [[ ! -d "$INVENTREE_BACKUP_DIR" ]]; then
+ echo "Creating directory $INVENTREE_BACKUP_DIR"
+ mkdir $INVENTREE_BACKUP_DIR
+fi
+
+# Check if "config.yaml" has been copied into the correct location
+if test -f "$INVENTREE_CONFIG_FILE"; then
+ echo "$INVENTREE_CONFIG_FILE exists - skipping"
+else
+ echo "Copying config file to $INVENTREE_CONFIG_FILE"
+ cp $INVENTREE_SRC_DIR/InvenTree/config_template.yaml $INVENTREE_CONFIG_FILE
+fi
+
+echo "Starting InvenTree server..."
+
+# Wait for the database to be ready
+cd $INVENTREE_MNG_DIR
+python manage.py wait_for_db
+
+sleep 10
+
+echo "Running InvenTree database migrations and collecting static files..."
+
+# We assume at this stage that the database is up and running
+# Ensure that the database schema are up to date
+python manage.py check || exit 1
+python manage.py migrate --noinput || exit 1
+python manage.py migrate --run-syncdb || exit 1
+python manage.py collectstatic --noinput || exit 1
+python manage.py clearsessions || exit 1
+
+# Launch a development server
+python manage.py runserver -a 0.0.0.0:$INVENTREE_WEB_PORT
\ No newline at end of file
diff --git a/docker/inventree/start_server.sh b/docker/start_prod_server.sh
similarity index 98%
rename from docker/inventree/start_server.sh
rename to docker/start_prod_server.sh
index 0436cd532f..1fc8f6d111 100644
--- a/docker/inventree/start_server.sh
+++ b/docker/start_prod_server.sh
@@ -43,4 +43,4 @@ python manage.py collectstatic --noinput || exit 1
python 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:8080
+gunicorn -c $INVENTREE_HOME/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$INVENTREE_WEB_PORT
\ No newline at end of file
diff --git a/docker/inventree/start_worker.sh b/docker/start_worker.sh
similarity index 88%
rename from docker/inventree/start_worker.sh
rename to docker/start_worker.sh
index 7d0921a7af..ba9eb14d65 100644
--- a/docker/inventree/start_worker.sh
+++ b/docker/start_worker.sh
@@ -11,4 +11,4 @@ python manage.py wait_for_db
sleep 10
# Now we can launch the background worker process
-python manage.py qcluster
+python manage.py qcluster
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index da6e219b96..1392531e29 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,11 @@
invoke>=1.4.0 # Invoke build tool
wheel>=0.34.2 # Wheel
-Django==3.0.7 # Django package
+Django==3.2 # Django package
pillow==8.1.1 # Image manipulation
-djangorestframework==3.11.2 # DRF framework
+djangorestframework==3.12.4 # DRF framework
django-dbbackup==3.3.0 # Database backup / restore functionality
django-cors-headers==3.2.0 # CORS headers extension for DRF
-django_filter==2.2.0 # Extended filtering options
+django-filter==2.4.0 # Extended filtering options
django-mptt==0.11.0 # Modified Preorder Tree Traversal
django-sql-utils==0.5.0 # Advanced query annotation / aggregation
django-markdownx==3.0.1 # Markdown form fields
@@ -13,7 +13,7 @@ django-markdownify==0.8.0 # Markdown rendering
coreapi==2.3.0 # API documentation
pygments==2.7.4 # Syntax highlighting
tablib==0.13.0 # Import / export data files
-django-crispy-forms==1.8.1 # Form helpers
+django-crispy-forms==1.11.2 # Form helpers
django-import-export==2.0.0 # Data import / export for admin interface
django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
flake8==3.8.3 # PEP checking
diff --git a/tasks.py b/tasks.py
index 895f3183ce..316e9b4bfb 100644
--- a/tasks.py
+++ b/tasks.py
@@ -154,6 +154,7 @@ def static(c):
as per Django requirements.
"""
+ manage(c, "prerender")
manage(c, "collectstatic --no-input")
@@ -173,7 +174,7 @@ def update(c):
"""
pass
-@task
+@task(post=[static])
def translate(c):
"""
Regenerate translation files.
@@ -183,7 +184,7 @@ def translate(c):
"""
# Translate applicable .py / .html / .js files
- manage(c, "makemessages -e py -e html -e js")
+ manage(c, "makemessages --all -e py,html,js")
manage(c, "compilemessages")
path = os.path.join('InvenTree', 'script', 'translation_stats.py')