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 %} - - - {% endif %} +
- -
-{% 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" %}
{% endif %} - -{% endif %} - {% endblock %} {% block js_ready %} @@ -66,19 +59,17 @@ InvenTree | Allocate Parts part: {{ build.part.pk }}, }; - {% for item in build.incomplete_outputs %} - // Get the build output as a javascript object - inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, - { - success: function(response) { - loadBuildOutputAllocationTable(buildInfo, response); - } - } - ); - {% endfor %} + {% if build.has_untracked_bom_items %} + // Load allocation table for un-tracked parts + loadBuildOutputAllocationTable(buildInfo, null); + {% endif %} + + function reloadTable() { + $('#allocation-table-untracked').bootstrapTable('refresh'); + } {% if build.active %} - $("#btn-allocate").on('click', function() { + $("#btn-auto-allocate").on('click', function() { launchModalForm( "{% url 'build-auto-allocate' build.id %}", { @@ -86,20 +77,12 @@ InvenTree | Allocate Parts } ); }); - + $('#btn-unallocate').on('click', function() { launchModalForm( "{% url 'build-unallocate' build.id %}", { - reload: true, - } - ); - }); - - $('#btn-create-output').click(function() { - launchModalForm('{% url "build-output-create" build.id %}', - { - reload: true, + success: reloadTable, } ); }); diff --git a/InvenTree/build/templates/build/allocation_card.html b/InvenTree/build/templates/build/allocation_card.html index 650257bb0d..3ce4a52aeb 100644 --- a/InvenTree/build/templates/build/allocation_card.html +++ b/InvenTree/build/templates/build/allocation_card.html @@ -7,23 +7,31 @@ -{% 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" %} +
+
+

{% trans "Stock Items" %}

+
+ {% include "stock_table.html" %}
{% endblock %} -{% block js_load %} -{{ block.super }} +
+ {% endblock %} + {% block js_ready %} {{ block.super }} - if (inventreeLoadInt("show-part-locs") == 1) { - $("#collapse-item-locations").collapse('show'); - } - - $("#collapse-item-locations").on('shown.bs.collapse', function() { - inventreeSave('show-part-locs', 1); - }); - - $("#collapse-item-locations").on('hidden.bs.collapse', function() { - inventreeDel('show-part-locs'); + enableNavbar({ + label: 'location', + toggleId: '#location-menu-toggle' }); {% if location %} @@ -261,7 +261,7 @@ ], params: { {% if location %} - location: {{ location.id }}, + location: {{ location.pk }}, {% endif %} part_detail: true, location_detail: true, diff --git a/InvenTree/stock/templates/stock/location_list.html b/InvenTree/stock/templates/stock/location_list.html deleted file mode 100644 index f9464c5fa3..0000000000 --- a/InvenTree/stock/templates/stock/location_list.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "collapse.html" %} -{% load i18n %} - -{% if roles.stock_location.view or roles.stock.view %} -{% block collapse_title %} -{% trans 'Sub-Locations' %}{{ children|length }} -{% endblock %} - -{% block collapse_content %} - -{% endblock %} -{% endif %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/location_navbar.html b/InvenTree/stock/templates/stock/location_navbar.html new file mode 100644 index 0000000000..0cb0c9d1eb --- /dev/null +++ b/InvenTree/stock/templates/stock/location_navbar.html @@ -0,0 +1,33 @@ +{% load i18n %} + + \ 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." %}
diff --git a/InvenTree/stock/templates/stock/sublocation.html b/InvenTree/stock/templates/stock/sublocation.html new file mode 100644 index 0000000000..24b034449e --- /dev/null +++ b/InvenTree/stock/templates/stock/sublocation.html @@ -0,0 +1,74 @@ +{% extends "stock/location.html" %} + +{% load static %} +{% load i18n %} +{% load inventree_extras %} + +{% block menubar %} +{% include "stock/location_navbar.html" with tab="sublocations" %} +{% endblock %} + + +{% block location_content %} + +
+
+

{% trans "Sublocations" %}

+
+ +
+
+ + +
+ +
+
+
+ +
+
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +loadStockLocationTable($('#sublocation-table'), { + params: { + {% if location %} + parent: {{ location.pk }}, + {% else %} + parent: 'null', + {% endif %} + } +}); + +linkButtonsToSelection( + $('#sublocation-table'), + [ + '#location-print-options', + ] +); + +$('#multi-location-print-label').click(function() { + + var selections = $('#sublocation-table').bootstrapTable('getSelections'); + + var locations = []; + + selections.forEach(function(loc) { + locations.push(loc.pk); + }); + + printStockLocationLabels(locations); +}) + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/tracking_delete.html b/InvenTree/stock/templates/stock/tracking_delete.html index b5dde45de2..79d5af5192 100644 --- a/InvenTree/stock/templates/stock/tracking_delete.html +++ b/InvenTree/stock/templates/stock/tracking_delete.html @@ -3,7 +3,7 @@ {% block pre_form_content %}
-Are you sure you want to delete this stock tracking entry? +{% trans "Are you sure you want to delete this stock tracking entry?" %}
{% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 5c6c678978..24e609fa4f 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -6,14 +6,21 @@ from django.conf.urls import url, include from . import views -# URL list for web interface -stock_location_detail_urls = [ - url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'), - url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'), - url(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'), +location_urls = [ + + url(r'^new/', views.StockLocationCreate.as_view(), name='stock-location-create'), + + url(r'^(?P\d+)/', include([ + url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'), + url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'), + url(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'), + + url(r'sublocation/', views.StockLocationDetail.as_view(template_name='stock/sublocation.html'), name='stock-location-sublocation'), + + # Anything else + url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'), + ])), - # Anything else - url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'), ] stock_item_detail_urls = [ @@ -49,9 +56,7 @@ stock_tracking_urls = [ stock_urls = [ # Stock location - url(r'^location/(?P\d+)/', include(stock_location_detail_urls)), - - url(r'^location/new/', views.StockLocationCreate.as_view(), name='stock-location-create'), + url(r'^location/', include(location_urls)), url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'), @@ -81,5 +86,7 @@ stock_urls = [ # Individual stock items url(r'^item/(?P\d+)/', include(stock_item_detail_urls)), + url(r'^sublocations/', views.StockIndex.as_view(template_name='stock/sublocation.html'), name='stock-sublocations'), + url(r'^.*$', views.StockIndex.as_view(), name='stock-index'), ] diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index f7154e5fbb..9b18e2d94c 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -2,12 +2,13 @@ {% load i18n %} {% load static %} {% load inventree_extras %} + {% block page_title %} -InvenTree | {% trans "Index" %} +{% inventree_title %} | {% trans "Index" %} {% endblock %} {% block content %} -

InvenTree

+

{% inventree_title %}


diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index b65ec00c00..5b5cd686f8 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -2,9 +2,10 @@ {% load static %} {% load i18n %} +{% load inventree_extras %} {% block page_title %} -InvenTree | {% trans "Search Results" %} +{% inventree_title %} | {% trans "Search Results" %} {% endblock %} {% block content %} @@ -145,6 +146,21 @@ InvenTree | {% trans "Search Results" %} ], }); + addItem('manufacturer-part', '{% trans "Manufacturer Parts" %}', 'fa-toolbox'); + + loadManufacturerPartTable( + "#table-manufacturer-part", + "{% url 'api-manufacturer-part-list' %}", + { + params: { + search: "{{ query }}", + part_detail: true, + supplier_detail: true, + manufacturer_detail: true + }, + } + ); + addItem('supplier-part', '{% trans "Supplier Parts" %}', 'fa-pallet'); loadSupplierPartTable( @@ -287,6 +303,15 @@ InvenTree | {% trans "Search Results" %} {% if roles.purchase_order.view or roles.sales_order.view %} addItemTitle('{% trans "Company" %}'); + addItem('manufacturer', '{% trans "Manufacturers" %}', 'fa-industry'); + + loadCompanyTable('#table-manufacturer', "{% url 'api-company-list' %}", { + params: { + search: "{{ query }}", + is_manufacturer: "true", + } + }); + {% if roles.purchase_order.view %} addItem('supplier', '{% trans "Suppliers" %}', 'fa-building'); @@ -305,16 +330,6 @@ InvenTree | {% trans "Search Results" %} } }); - addItem('manufacturer', '{% trans "Manufacturers" %}', 'fa-industry'); - - loadCompanyTable('#table-manufacturer', "{% url 'api-company-list' %}", { - params: { - search: "{{ query }}", - is_manufacturer: "true", - } - }); - - {% endif %} {% if roles.sales_order.view %} diff --git a/InvenTree/templates/InvenTree/settings/global.html b/InvenTree/templates/InvenTree/settings/global.html index c234bd1379..a0347490d0 100644 --- a/InvenTree/templates/InvenTree/settings/global.html +++ b/InvenTree/templates/InvenTree/settings/global.html @@ -16,6 +16,7 @@ {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" icon="fa-info-circle" %} + {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE_TITLE" icon="fa-info-circle" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_BASE_URL" icon="fa-globe" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %} diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index fe9fd00e53..f63860012c 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -2,9 +2,10 @@ {% load i18n %} {% load static %} +{% load inventree_extras %} {% block page_title %} -InvenTree | {% trans "Settings" %} +{% inventree_title %} | {% trans "Settings" %} {% endblock %} {% block content %} diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 140971a8ce..970d9f1016 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -60,7 +60,7 @@ {% block page_title %} -InvenTree +{% inventree_title %} {% endblock %} @@ -143,20 +143,21 @@ InvenTree - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js index beb4110df6..5b62955499 100644 --- a/InvenTree/templates/js/bom.js +++ b/InvenTree/templates/js/bom.js @@ -268,6 +268,10 @@ function loadBomTable(table, options) { field: 'optional', title: '{% trans "Optional" %}', searchable: false, + formatter: function(value) { + if (value == '1') return '{% trans "true" %}'; + if (value == '0') return '{% trans "false" %}'; + } }); cols.push({ diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index 539d1565aa..d4ceb9c909 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -32,12 +32,17 @@ function newBuildOrder(options={}) { } -function makeBuildOutputActionButtons(output, buildInfo) { +function makeBuildOutputActionButtons(output, buildInfo, lines) { /* Generate action buttons for a build output. */ var buildId = buildInfo.pk; - var outputId = output.pk; + + if (output) { + outputId = output.pk; + } else { + outputId = 'untracked'; + } var panel = `#allocation-panel-${outputId}`; @@ -50,35 +55,42 @@ function makeBuildOutputActionButtons(output, buildInfo) { var html = `
`; - // Add a button to "auto allocate" against the build - html += makeIconButton( - 'fa-magic icon-blue', 'button-output-auto', outputId, - '{% trans "Auto-allocate stock items to this output" %}', - ); + // "Auto" allocation only works for untracked stock items + if (!output && lines > 0) { + html += makeIconButton( + 'fa-magic icon-blue', 'button-output-auto', outputId, + '{% trans "Auto-allocate stock items to this output" %}', + ); + } - // Add a button to "complete" the particular build output - html += makeIconButton( - 'fa-check icon-green', 'button-output-complete', outputId, - '{% trans "Complete build output" %}', - { - //disabled: true - } - ); + if (lines > 0) { + // Add a button to "cancel" the particular build output (unallocate) + html += makeIconButton( + 'fa-minus-circle icon-red', 'button-output-unallocate', outputId, + '{% trans "Unallocate stock from build output" %}', + ); + } - // Add a button to "cancel" the particular build output (unallocate) - html += makeIconButton( - 'fa-minus-circle icon-red', 'button-output-unallocate', outputId, - '{% trans "Unallocate stock from build output" %}', - ); - // Add a button to "delete" the particular build output - html += makeIconButton( - 'fa-trash-alt icon-red', 'button-output-delete', outputId, - '{% trans "Delete build output" %}', - ); + if (output) { - // Add a button to "destroy" the particular build output (mark as damaged, scrap) - // TODO + // Add a button to "complete" the particular build output + html += makeIconButton( + 'fa-check icon-green', 'button-output-complete', outputId, + '{% trans "Complete build output" %}', + { + //disabled: true + } + ); + + // Add a button to "delete" the particular build output + html += makeIconButton( + 'fa-trash-alt icon-red', 'button-output-delete', outputId, + '{% trans "Delete build output" %}', + ); + + // TODO - Add a button to "destroy" the particular build output (mark as damaged, scrap) + } html += '
'; @@ -90,7 +102,6 @@ function makeBuildOutputActionButtons(output, buildInfo) { launchModalForm(`/build/${buildId}/auto-allocate/`, { data: { - output: outputId, }, success: reloadTable, } @@ -98,11 +109,14 @@ function makeBuildOutputActionButtons(output, buildInfo) { }); $(panel).find(`#button-output-complete-${outputId}`).click(function() { + + var pk = $(this).attr('pk'); + launchModalForm( `/build/${buildId}/complete-output/`, { data: { - output: outputId, + output: pk, }, reload: true, } @@ -110,24 +124,30 @@ function makeBuildOutputActionButtons(output, buildInfo) { }); $(panel).find(`#button-output-unallocate-${outputId}`).click(function() { + + var pk = $(this).attr('pk'); + launchModalForm( `/build/${buildId}/unallocate/`, { success: reloadTable, data: { - output: outputId, + output: pk, } } ); }); $(panel).find(`#button-output-delete-${outputId}`).click(function() { + + var pk = $(this).attr('pk'); + launchModalForm( `/build/${buildId}/delete-output/`, { reload: true, data: { - output: outputId + output: pk } } ); @@ -152,13 +172,21 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var outputId = null; - outputId = output.pk; + if (output) { + outputId = output.pk; + } else { + outputId = 'untracked'; + } var table = options.table; if (options.table == null) { table = `#allocation-table-${outputId}`; } + + // If an "output" is specified, then only "trackable" parts are allocated + // Otherwise, only "untrackable" parts are allowed + var trackable = ! !output; function reloadTable() { // Reload the entire build allocation table @@ -168,7 +196,13 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { function requiredQuantity(row) { // Return the requied quantity for a given row - return row.quantity * output.quantity; + if (output) { + // "Tracked" parts are calculated against individual build outputs + return row.quantity * output.quantity; + } else { + // "Untracked" parts are specified against the build itself + return row.quantity * buildInfo.quantity; + } } function sumAllocations(row) { @@ -300,6 +334,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { queryParams: { part: partId, sub_part_detail: true, + sub_part_trackable: trackable, }, formatNoMatches: function() { return '{% trans "No BOM items found" %}'; @@ -310,11 +345,19 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for this build output + var params = { + build: buildId, + } + + if (output) { + params.sub_part_trackable = true; + params.output = outputId; + } else { + params.sub_part_trackable = false; + } + inventreeGet('/api/build/item/', - { - build: buildId, - output: outputId, - }, + params, { success: function(data) { // Iterate through the returned data, and group by the part they point to @@ -355,8 +398,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Calculate the total allocated quantity var allocatedQuantity = sumAllocations(tableRow); + var requiredQuantity = 0; + + if (output) { + requiredQuantity = tableRow.quantity * output.quantity; + } else { + requiredQuantity = tableRow.quantity * buildInfo.quantity; + } + // Is this line item fully allocated? - if (allocatedQuantity >= (tableRow.quantity * output.quantity)) { + if (allocatedQuantity >= requiredQuantity) { allocatedLines += 1; } @@ -367,16 +418,21 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Update the total progress for this build output var buildProgress = $(`#allocation-panel-${outputId}`).find($(`#output-progress-${outputId}`)); - var progress = makeProgressBar( - allocatedLines, - totalLines - ); + if (totalLines > 0) { - buildProgress.html(progress); + var progress = makeProgressBar( + allocatedLines, + totalLines + ); + + buildProgress.html(progress); + } else { + buildProgress.html(''); + } // Update the available actions for this build output - makeBuildOutputActionButtons(output, buildInfo); + makeBuildOutputActionButtons(output, buildInfo, totalLines); } } ); @@ -600,6 +656,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { }, ] }); + + // Initialize the action buttons + makeBuildOutputActionButtons(output, buildInfo, 0); } @@ -654,7 +713,7 @@ function loadBuildTable(table, options) { field: 'reference', title: '{% trans "Build" %}', sortable: true, - switchable: false, + switchable: true, formatter: function(value, row, index, field) { var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}"; @@ -675,6 +734,7 @@ function loadBuildTable(table, options) { { field: 'title', title: '{% trans "Description" %}', + switchable: true, }, { field: 'part', @@ -725,7 +785,7 @@ function loadBuildTable(table, options) { }, { field: 'completion_date', - title: '{% trans "Completed" %}', + title: '{% trans "Completion Date" %}', sortable: true, }, ], diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index 601d4a5370..d258c8bab1 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -101,6 +101,104 @@ function loadCompanyTable(table, url, options={}) { } +function loadManufacturerPartTable(table, url, options) { + /* + * Load manufacturer part table + * + */ + + // Query parameters + var params = options.params || {}; + + // Load filters + var filters = loadTableFilters("manufacturer-part"); + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList("manufacturer-part", $(table)); + + $(table).inventreeTable({ + url: url, + method: 'get', + original: params, + queryParams: filters, + name: 'manufacturerparts', + groupBy: false, + formatNoMatches: function() { return "{% trans "No manufacturer parts found" %}"; }, + columns: [ + { + checkbox: true, + switchable: false, + }, + { + visible: params['part_detail'], + switchable: params['part_detail'], + sortable: true, + field: 'part_detail.full_name', + title: '{% trans "Part" %}', + formatter: function(value, row, index, field) { + + var url = `/part/${row.part}/`; + + var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value, url); + + if (row.part_detail.is_template) { + html += ``; + } + + if (row.part_detail.assembly) { + html += ``; + } + + if (!row.part_detail.active) { + html += `{% trans "Inactive" %}`; + } + + return html; + } + }, + { + sortable: true, + field: 'manufacturer', + title: '{% trans "Manufacturer" %}', + formatter: function(value, row, index, field) { + if (value && row.manufacturer_detail) { + var name = row.manufacturer_detail.name; + var url = `/company/${value}/`; + var html = imageHoverIcon(row.manufacturer_detail.image) + renderLink(name, url); + + return html; + } else { + return "-"; + } + } + }, + { + sortable: true, + field: 'MPN', + title: '{% trans "MPN" %}', + formatter: function(value, row, index, field) { + return renderLink(value, `/manufacturer-part/${row.pk}/`); + } + }, + { + field: 'link', + title: '{% trans "Link" %}', + formatter: function(value, row, index, field) { + if (value) { + return renderLink(value, value); + } else { + return ''; + } + } + }, + ], + }); +} + + function loadSupplierPartTable(table, url, options) { /* * Load supplier part table @@ -133,10 +231,11 @@ function loadSupplierPartTable(table, url, options) { switchable: false, }, { + visible: params['part_detail'], + switchable: params['part_detail'], sortable: true, field: 'part_detail.full_name', title: '{% trans "Part" %}', - switchable: false, formatter: function(value, row, index, field) { var url = `/part/${row.part}/`; @@ -183,6 +282,8 @@ function loadSupplierPartTable(table, url, options) { } }, { + visible: params['manufacturer_detail'], + switchable: params['manufacturer_detail'], sortable: true, field: 'manufacturer', title: '{% trans "Manufacturer" %}', @@ -199,9 +300,18 @@ function loadSupplierPartTable(table, url, options) { } }, { + visible: params['manufacturer_detail'], + switchable: params['manufacturer_detail'], sortable: true, field: 'MPN', title: '{% trans "MPN" %}', + formatter: function(value, row, index, field) { + if (value && row.manufacturer_part) { + return renderLink(value, `/manufacturer-part/${row.manufacturer_part.pk}/`); + } else { + return "-"; + } + } }, { field: 'link', diff --git a/InvenTree/templates/js/filters.js b/InvenTree/templates/js/filters.js index 01b74763e0..612af8e03c 100644 --- a/InvenTree/templates/js/filters.js +++ b/InvenTree/templates/js/filters.js @@ -164,11 +164,11 @@ function getFilterOptionList(tableKey, filterKey) { return { '1': { key: '1', - value: 'true', + value: '{% trans "true" %}', }, '0': { key: '0', - value: 'false', + value: '{% trans "false" %}', }, }; } else if ('options' in settings) { @@ -394,8 +394,8 @@ function getFilterOptionValue(tableKey, filterKey, valueKey) { // Lookup for boolean options if (filter.type == 'bool') { - if (value == '1') return 'true'; - if (value == '0') return 'false'; + if (value == '1') return '{% trans "true" %}'; + if (value == '0') return '{% trans "false" %}'; return value; } diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index cefc2af8a7..e3e7190952 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -1,4 +1,5 @@ {% load i18n %} +{% load inventree_extras %} /* Part API functions * Requires api.js to be loaded first @@ -506,6 +507,82 @@ function loadPartTable(table, url, options={}) { }); } + +function loadPartCategoryTable(table, options) { + /* Display a table of part categories */ + + var params = options.params || {}; + + var filterListElement = options.filterList || '#filter-list-category'; + + var filters = {}; + + var filterKey = options.filterKey || options.name || 'category'; + + if (!options.disableFilters) { + filters = loadTableFilters(filterKey); + } + + var original = {}; + + for (var key in params) { + original[key] = params[key]; + filters[key] = params[key]; + } + + setupFilterList(filterKey, table, filterListElement); + + table.inventreeTable({ + method: 'get', + url: options.url || '{% url "api-part-category-list" %}', + queryParams: filters, + sidePagination: 'server', + name: 'category', + original: original, + showColumns: true, + columns: [ + { + checkbox: true, + title: '{% trans "Select" %}', + searchable: false, + switchable: false, + visible: false, + }, + { + field: 'name', + title: '{% trans "Name" %}', + switchable: true, + sortable: true, + formatter: function(value, row) { + return renderLink( + value, + `/part/category/${row.pk}/` + ); + } + }, + { + field: 'description', + title: '{% trans "Description" %}', + switchable: true, + sortable: false, + }, + { + field: 'pathstring', + title: '{% trans "Path" %}', + switchable: true, + sortable: false, + }, + { + field: 'parts', + title: '{% trans "Parts" %}', + switchable: true, + sortable: false, + } + ] + }); +} + + function yesNoLabel(value) { if (value) { return `{% trans "YES" %}`; diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index b163bc89f3..c10e432f5e 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -354,7 +354,7 @@ function loadStockTable(table, options) { var html = imageHoverIcon(row.part_detail.thumbnail); html += row.part_detail.full_name; - html += ` (${data.length} items)`; + html += ` (${data.length} {% trans "items" %})`; html += makePartIcons(row.part_detail); @@ -446,7 +446,7 @@ function loadStockTable(table, options) { }); if (batches.length > 1) { - return "" + batches.length + " batches"; + return "" + batches.length + " {% trans 'batches' %}"; } else if (batches.length == 1) { if (batches[0]) { return batches[0]; @@ -473,9 +473,9 @@ function loadStockTable(table, options) { // Single location, easy! return locations[0]; } else if (locations.length > 1) { - return "In " + locations.length + " locations"; + return "In " + locations.length + " {% trans 'locations' %}"; } else { - return "{% trans "Undefined location" %}"; + return "{% trans 'Undefined location' %}"; } } else if (field == 'notes') { var notes = []; @@ -897,6 +897,83 @@ function loadStockTable(table, options) { }); } +function loadStockLocationTable(table, options) { + /* Display a table of stock locations */ + + var params = options.params || {}; + + var filterListElement = options.filterList || '#filter-list-location'; + + var filters = {}; + + var filterKey = options.filterKey || options.name || 'location'; + + if (!options.disableFilters) { + filters = loadTableFilters(filterKey); + } + + var original = {}; + + for (var key in params) { + original[key] = params[key]; + } + + setupFilterList(filterKey, table, filterListElement); + + for (var key in params) { + filters[key] = params[key]; + } + + table.inventreeTable({ + method: 'get', + url: options.url || '{% url "api-location-list" %}', + queryParams: filters, + sidePagination: 'server', + name: 'location', + original: original, + showColumns: true, + columns: [ + { + checkbox: true, + title: '{% trans "Select" %}', + searchable: false, + switchable: false, + }, + { + field: 'name', + title: '{% trans "Name" %}', + switchable: true, + sortable: true, + formatter: function(value, row) { + return renderLink( + value, + `/stock/location/${row.pk}/` + ); + }, + }, + { + field: 'description', + title: '{% trans "Description" %}', + switchable: true, + sortable: false, + }, + { + field: 'pathstring', + title: '{% trans "Path" %}', + switchable: true, + sortable: false, + }, + { + field: 'items', + title: '{% trans "Stock Items" %}', + switchable: true, + sortable: false, + sortName: 'item_count', + } + ] + }); +} + function loadStockTrackingTable(table, options) { var cols = [ @@ -1219,7 +1296,7 @@ function loadInstalledInTable(table, options) { // Add some buttons yo! html += `
`; - html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans "Uninstall stock item" %}"); + html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans 'Uninstall stock item' %}"); html += `
`; diff --git a/InvenTree/templates/js/table_filters.js b/InvenTree/templates/js/table_filters.js index ba73244c74..775f0d9803 100644 --- a/InvenTree/templates/js/table_filters.js +++ b/InvenTree/templates/js/table_filters.js @@ -62,6 +62,28 @@ function getAvailableTableFilters(tableKey) { }; } + // Filters for "stock location" table + if (tableKey == "location") { + return { + cascade: { + type: 'bool', + title: '{% trans "Include sublocations" %}', + description: '{% trans "Include locations" %}', + } + }; + } + + // Filters for "part category" table + if (tableKey == "category") { + return { + cascade: { + type: 'bool', + title: '{% trans "Include subcategories" %}', + description: '{% trans "Include subcategories" %}', + } + }; + } + // Filters for the "customer stock" table (really a subset of "stock") if (tableKey == "customerstock") { return { diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index acd71f0cd8..55e0e06018 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -59,11 +59,17 @@ {% endif %}
{% endif %} +