From a742df2c123345f9617bfecd5c64b783efb2c075 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 10:10:58 +1100 Subject: [PATCH 01/26] Adds 'length' and 'width' fields to label models --- .../migrations/0006_auto_20210222_0952.py | 34 +++++++++++++++++++ InvenTree/label/models.py | 16 ++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 InvenTree/label/migrations/0006_auto_20210222_0952.py diff --git a/InvenTree/label/migrations/0006_auto_20210222_0952.py b/InvenTree/label/migrations/0006_auto_20210222_0952.py new file mode 100644 index 0000000000..62e9d1a7f5 --- /dev/null +++ b/InvenTree/label/migrations/0006_auto_20210222_0952.py @@ -0,0 +1,34 @@ +# Generated by Django 3.0.7 on 2021-02-21 22:52 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('label', '0005_auto_20210113_2302'), + ] + + operations = [ + migrations.AddField( + model_name='stockitemlabel', + name='length', + field=models.FloatField(default=20, help_text='Label length, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Length [mm]'), + ), + migrations.AddField( + model_name='stockitemlabel', + name='width', + field=models.FloatField(default=10, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), + ), + migrations.AddField( + model_name='stocklocationlabel', + name='length', + field=models.FloatField(default=20, help_text='Label length, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Length [mm]'), + ), + migrations.AddField( + model_name='stocklocationlabel', + name='width', + field=models.FloatField(default=10, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), + ), + ] diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index 9a98810d28..ae00bc0dc1 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -11,7 +11,7 @@ import io from blabel import LabelWriter from django.db import models -from django.core.validators import FileExtensionValidator +from django.core.validators import FileExtensionValidator, MinValueValidator from django.core.exceptions import ValidationError, FieldError from django.utils.translation import gettext_lazy as _ @@ -92,6 +92,20 @@ class LabelTemplate(models.Model): help_text=_('Label template is enabled'), ) + length = models.FloatField( + default=20, + verbose_name=_('Length [mm]'), + help_text=_('Label length, specified in mm'), + validators=[MinValueValidator(2)] + ) + + width = models.FloatField( + default=10, + verbose_name=('Width [mm]'), + help_text=_('Label width, specified in mm'), + validators=[MinValueValidator(2)] + ) + def get_record_data(self, items): """ Return a list of dict objects, one for each item. From 828b3adc49cdf4791a02cefc89384593b674a8c3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 14:40:40 +1100 Subject: [PATCH 02/26] Update requirements.txt - Remove blabel - Remove django-qr-code - Add python-barcode - Add qrcode --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index e0fdd6dfd9..cf05148ebf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ wheel>=0.34.2 # Wheel Django==3.0.7 # Django package pillow==7.1.0 # Image manipulation -blabel==0.1.3 # Simple PDF label printing djangorestframework==3.10.3 # DRF framework django-dbbackup==3.3.0 # Database backup / restore functionality django-cors-headers==3.2.0 # CORS headers extension for DRF @@ -16,7 +15,6 @@ tablib==0.13.0 # Import / export data files django-crispy-forms==1.8.1 # 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 -django-qr-code==1.2.0 # Generate QR codes flake8==3.8.3 # PEP checking pep8-naming==0.11.1 # PEP naming convention extension coverage==5.3 # Unit test coverage @@ -31,5 +29,7 @@ certifi # Certifi is (most likely) installed through one django-error-report==0.2.0 # Error report viewer for the admin interface django-test-migrations==1.1.0 # Unit testing for database migrations django-migration-linter==2.5.0 # Linting checks for database migrations +python-barcode[images]==0.13.1 # Barcode generator +qrcode[pil]==6.1 # QR code generator inventree # Install the latest version of the InvenTree API python library From 23da591c2201fe964029d6125c9e7ce2d75f30fd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 15:00:12 +1100 Subject: [PATCH 03/26] Remove old dependency on django_qr_code --- InvenTree/InvenTree/settings.py | 3 -- InvenTree/InvenTree/urls.py | 3 -- InvenTree/report/templatetags/barcode.py | 64 ++++++++++++++++++++++++ InvenTree/templates/qr_code.html | 10 ++-- 4 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 InvenTree/report/templatetags/barcode.py diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 7e3ed2c7a6..0ab85a16cf 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -204,7 +204,6 @@ INSTALLED_APPS = [ 'crispy_forms', # Improved form rendering 'import_export', # Import / export tables to file 'django_cleanup.apps.CleanupConfig', # Automatically delete orphaned MEDIA files - 'qr_code', # Generate QR codes 'mptt', # Modified Preorder Tree Traversal 'markdownx', # Markdown editing 'markdownify', # Markdown template rendering @@ -407,8 +406,6 @@ CACHES = { } } -QR_CODE_CACHE_ALIAS = 'qr-code' - # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 1df5e83d5d..9f7be3fce1 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -9,7 +9,6 @@ from django.conf.urls import url, include from django.urls import path from django.contrib import admin from django.contrib.auth import views as auth_views -from qr_code import urls as qr_code_urls from company.urls import company_urls from company.urls import supplier_part_urls @@ -141,8 +140,6 @@ urlpatterns = [ url(r'^admin/shell/', include('django_admin_shell.urls')), url(r'^admin/', admin.site.urls, name='inventree-admin'), - url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')), - url(r'^index/', IndexView.as_view(), name='index'), url(r'^search/', SearchView.as_view(), name='search'), url(r'^stats/', DatabaseStatsView.as_view(), name='stats'), diff --git a/InvenTree/report/templatetags/barcode.py b/InvenTree/report/templatetags/barcode.py new file mode 100644 index 0000000000..18afdea81e --- /dev/null +++ b/InvenTree/report/templatetags/barcode.py @@ -0,0 +1,64 @@ +""" +Template tags for rendering various barcodes +""" + +import os +import base64 + +from io import BytesIO + + +from django import template + +import qrcode +import barcode + +register = template.Library() + + +def image_data(img, fmt='PNG'): + """ + Convert an image into HTML renderable data + + Returns a string ``data:image/FMT;base64,xxxxxxxxx`` which can be rendered to an tag + """ + + buffered = BytesIO() + img.save(buffered, format=fmt) + + img_str = base64.b64encode(buffered.getvalue()) + + return f"data:image/{fmt.lower()};charset=utf-8;base64," + img_str.decode() + + +@register.simple_tag() +def qr_code(data, **kwargs): + """ + Return a byte-encoded QR code image + + Optional kwargs + --------------- + + fill_color: Fill color (default = black) + back_color: Background color (default = white) + """ + + # Construct "default" values + params = dict( + box_size=20, + border=1, + ) + + fill_color = kwargs.pop('fill_color', 'black') + back_color = kwargs.pop('back_color', 'white') + + params.update(**kwargs) + + qr = qrcode.QRCode(**params) + + qr.add_data(data, optimize=20) + qr.make(fit=True) + + qri = qr.make_image(fill_color=fill_color, back_color=back_color) + + return image_data(qri) \ No newline at end of file diff --git a/InvenTree/templates/qr_code.html b/InvenTree/templates/qr_code.html index 8bb288f797..c6c2f44b87 100644 --- a/InvenTree/templates/qr_code.html +++ b/InvenTree/templates/qr_code.html @@ -1,12 +1,14 @@ -{% load qr_code %} +{% load barcode %} +{% load i18n %}
{% if qr_data %}
- QR Code +
{% else %} - Error:
- {{ error_msg }} +
+ {% trans "QR data not provided" %} +
{% endif %}
\ No newline at end of file From caf4c293d991868e259e67f77bcd1337ef5569fe Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 15:15:25 +1100 Subject: [PATCH 04/26] Rename "barcode" module to "barcodes" to prevent import shadowing - Add 'barcode' support --- InvenTree/InvenTree/static/css/inventree.css | 1 + InvenTree/InvenTree/urls.py | 2 +- InvenTree/{barcode => barcodes}/__init__.py | 0 InvenTree/{barcode => barcodes}/api.py | 2 +- InvenTree/{barcode => barcodes}/barcode.py | 2 +- .../plugins/digikey_barcode.py | 2 +- .../plugins/inventree_barcode.py | 2 +- InvenTree/{barcode => barcodes}/tests.py | 0 InvenTree/report/templatetags/barcode.py | 30 +++++++++++++++---- InvenTree/templates/qr_code.html | 2 +- 10 files changed, 32 insertions(+), 11 deletions(-) rename InvenTree/{barcode => barcodes}/__init__.py (100%) rename InvenTree/{barcode => barcodes}/api.py (99%) rename InvenTree/{barcode => barcodes}/barcode.py (98%) rename InvenTree/{barcode => barcodes}/plugins/digikey_barcode.py (85%) rename InvenTree/{barcode => barcodes}/plugins/inventree_barcode.py (98%) rename InvenTree/{barcode => barcodes}/tests.py (100%) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index a0053e5f53..34c101e86d 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -125,6 +125,7 @@ .qr-container { width: 100%; align-content: center; + object-fit: fill; } .navbar-brand { diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 9f7be3fce1..c5b439c0be 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -20,7 +20,7 @@ from stock.urls import stock_urls from build.urls import build_urls from order.urls import order_urls -from barcode.api import barcode_api_urls +from barcodes.api import barcode_api_urls from common.api import common_api_urls from part.api import part_api_urls, bom_api_urls from company.api import company_api_urls diff --git a/InvenTree/barcode/__init__.py b/InvenTree/barcodes/__init__.py similarity index 100% rename from InvenTree/barcode/__init__.py rename to InvenTree/barcodes/__init__.py diff --git a/InvenTree/barcode/api.py b/InvenTree/barcodes/api.py similarity index 99% rename from InvenTree/barcode/api.py rename to InvenTree/barcodes/api.py index f8a2c37329..d727f5a778 100644 --- a/InvenTree/barcode/api.py +++ b/InvenTree/barcodes/api.py @@ -12,7 +12,7 @@ from rest_framework.views import APIView from stock.models import StockItem from stock.serializers import StockItemSerializer -from barcode.barcode import load_barcode_plugins, hash_barcode +from barcodes.barcode import load_barcode_plugins, hash_barcode class BarcodeScan(APIView): diff --git a/InvenTree/barcode/barcode.py b/InvenTree/barcodes/barcode.py similarity index 98% rename from InvenTree/barcode/barcode.py rename to InvenTree/barcodes/barcode.py index 004182e0be..412065bf75 100644 --- a/InvenTree/barcode/barcode.py +++ b/InvenTree/barcodes/barcode.py @@ -5,7 +5,7 @@ import hashlib import logging from InvenTree import plugins as InvenTreePlugins -from barcode import plugins as BarcodePlugins +from barcodes import plugins as BarcodePlugins from stock.models import StockItem from stock.serializers import StockItemSerializer, LocationSerializer diff --git a/InvenTree/barcode/plugins/digikey_barcode.py b/InvenTree/barcodes/plugins/digikey_barcode.py similarity index 85% rename from InvenTree/barcode/plugins/digikey_barcode.py rename to InvenTree/barcodes/plugins/digikey_barcode.py index 1a0825e03c..656cdfa6f4 100644 --- a/InvenTree/barcode/plugins/digikey_barcode.py +++ b/InvenTree/barcodes/plugins/digikey_barcode.py @@ -4,7 +4,7 @@ DigiKey barcode decoding """ -from barcode.barcode import BarcodePlugin +from barcodes.barcode import BarcodePlugin class DigikeyBarcodePlugin(BarcodePlugin): diff --git a/InvenTree/barcode/plugins/inventree_barcode.py b/InvenTree/barcodes/plugins/inventree_barcode.py similarity index 98% rename from InvenTree/barcode/plugins/inventree_barcode.py rename to InvenTree/barcodes/plugins/inventree_barcode.py index 93fb58cbc0..842f9029aa 100644 --- a/InvenTree/barcode/plugins/inventree_barcode.py +++ b/InvenTree/barcodes/plugins/inventree_barcode.py @@ -13,7 +13,7 @@ references model objects actually exist in the database. import json -from barcode.barcode import BarcodePlugin +from barcodes.barcode import BarcodePlugin from stock.models import StockItem, StockLocation from part.models import Part diff --git a/InvenTree/barcode/tests.py b/InvenTree/barcodes/tests.py similarity index 100% rename from InvenTree/barcode/tests.py rename to InvenTree/barcodes/tests.py diff --git a/InvenTree/report/templatetags/barcode.py b/InvenTree/report/templatetags/barcode.py index 18afdea81e..9b826d8921 100644 --- a/InvenTree/report/templatetags/barcode.py +++ b/InvenTree/report/templatetags/barcode.py @@ -10,8 +10,8 @@ from io import BytesIO from django import template -import qrcode -import barcode +import qrcode as python_qrcode +import barcode as python_barcode register = template.Library() @@ -32,7 +32,7 @@ def image_data(img, fmt='PNG'): @register.simple_tag() -def qr_code(data, **kwargs): +def qrcode(data, **kwargs): """ Return a byte-encoded QR code image @@ -54,11 +54,31 @@ def qr_code(data, **kwargs): params.update(**kwargs) - qr = qrcode.QRCode(**params) + qr = python_qrcode.QRCode(**params) qr.add_data(data, optimize=20) qr.make(fit=True) qri = qr.make_image(fill_color=fill_color, back_color=back_color) - return image_data(qri) \ No newline at end of file + return image_data(qri) + + +@register.simple_tag() +def barcode(data, barcode_class='code128', **kwargs): + """ + Render a barcode + """ + + constructor = python_barcode.get_barcode_class(barcode_class) + + data = str(data).zfill(constructor.digits) + + writer = python_barcode.writer.ImageWriter + + barcode_image = constructor(data, writer=writer()) + + image = barcode_image.render(writer_options=kwargs) + + # Render to byte-encoded PNG + return image_data(image) diff --git a/InvenTree/templates/qr_code.html b/InvenTree/templates/qr_code.html index c6c2f44b87..8964ef02be 100644 --- a/InvenTree/templates/qr_code.html +++ b/InvenTree/templates/qr_code.html @@ -4,7 +4,7 @@
{% if qr_data %}
- +
{% else %}
From da715d738163f09afb3fbdf897a7ea1b3ceae6ab Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 16:12:13 +1100 Subject: [PATCH 05/26] Refactoring label printing --- InvenTree/InvenTree/settings.py | 1 + InvenTree/label/api.py | 133 ++++++++------ ...222_0952.py => 0006_auto_20210222_1535.py} | 14 +- InvenTree/label/models.py | 170 ++++++++++++------ .../label/templates/label/label_base.html | 28 +++ InvenTree/report/api.py | 2 +- InvenTree/report/models.py | 2 +- 7 files changed, 228 insertions(+), 122 deletions(-) rename InvenTree/label/migrations/{0006_auto_20210222_0952.py => 0006_auto_20210222_1535.py} (67%) create mode 100644 InvenTree/label/templates/label/label_base.html diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 0ab85a16cf..70760624c6 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -249,6 +249,7 @@ TEMPLATES = [ os.path.join(BASE_DIR, 'templates'), # Allow templates in the reporting directory to be accessed os.path.join(MEDIA_ROOT, 'report'), + os.path.join(MEDIA_ROOT, 'label'), ], 'APP_DIRS': True, 'OPTIONS': { diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index b2bfe9164f..899b00b0ec 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -6,6 +6,7 @@ import sys from django.utils.translation import ugettext as _ from django.conf.urls import url, include from django.core.exceptions import ValidationError, FieldError +from django.http import HttpResponse from django_filters.rest_framework import DjangoFilterBackend @@ -13,6 +14,7 @@ from rest_framework import generics, filters from rest_framework.response import Response import InvenTree.helpers +import common.models from stock.models import StockItem, StockLocation @@ -40,6 +42,74 @@ class LabelListView(generics.ListAPIView): ] +class LabelPrintMixin: + """ + Mixin for printing labels + """ + + def print(self, request, items_to_print): + """ + Print this label template against a number of pre-validated items + """ + + if len(items_to_print) == 0: + # No valid items provided, return an error message + data = { + 'error': _('No valid objects provided to template'), + } + + return Response(data, status=400) + + outputs = [] + + # In debug mode, generate single HTML output, rather than PDF + debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE') + + # Merge one or more PDF files into a single download + for item in items_to_print: + label = self.get_object() + label.object_to_print = item + + if debug_mode: + outputs.append(label.render_as_string(request)) + else: + outputs.append(label.render(request)) + + if debug_mode: + """ + Contatenate all rendered templates into a single HTML string, + and return the string as a HTML response. + """ + + html = "\n".join(outputs) + + return HttpResponse(html) + else: + """ + Concatenate all rendered pages into a single PDF object, + and return the resulting document! + """ + + pages = [] + + if len(outputs) > 1: + # If more than one output is generated, merge them into a single file + for output in outputs: + doc = output.get_document() + for page in doc.pages: + pages.append(page) + + pdf = outputs[0].get_document().copy(pages).write_pdf() + else: + pdf = outputs[0].get_document().write_pdf() + + return InvenTree.helpers.DownloadFile( + pdf, + 'inventree_label.pdf', + content_type='application/pdf' + ) + + class StockItemLabelMixin: """ Mixin for extracting stock items from query params @@ -158,7 +228,7 @@ class StockItemLabelDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = StockItemLabelSerializer -class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin): +class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin, LabelPrintMixin): """ API endpoint for printing a StockItemLabel object """ @@ -173,34 +243,7 @@ class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin): items = self.get_items() - if len(items) == 0: - # No valid items provided, return an error message - data = { - 'error': _('Must provide valid StockItem(s)'), - } - - return Response(data, status=400) - - label = self.get_object() - - try: - pdf = label.render(items) - except: - - e = sys.exc_info()[1] - - data = { - 'error': _('Error during label rendering'), - 'message': str(e), - } - - return Response(data, status=400) - - return InvenTree.helpers.DownloadFile( - pdf.getbuffer(), - 'stock_item_label.pdf', - content_type='application/pdf' - ) + return self.print(request, items) class StockLocationLabelMixin: @@ -320,7 +363,7 @@ class StockLocationLabelDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = StockLocationLabelSerializer -class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin): +class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin, LabelPrintMixin): """ API endpoint for printing a StockLocationLabel object """ @@ -332,35 +375,7 @@ class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin) locations = self.get_locations() - if len(locations) == 0: - # No valid locations provided - return an error message - - return Response( - { - 'error': _('Must provide valid StockLocation(s)'), - }, - status=400, - ) - - label = self.get_object() - - try: - pdf = label.render(locations) - except: - e = sys.exc_info()[1] - - data = { - 'error': _('Error during label rendering'), - 'message': str(e), - } - - return Response(data, status=400) - - return InvenTree.helpers.DownloadFile( - pdf.getbuffer(), - 'stock_location_label.pdf', - content_type='application/pdf' - ) + return self.print(request, locations) label_api_urls = [ diff --git a/InvenTree/label/migrations/0006_auto_20210222_0952.py b/InvenTree/label/migrations/0006_auto_20210222_1535.py similarity index 67% rename from InvenTree/label/migrations/0006_auto_20210222_0952.py rename to InvenTree/label/migrations/0006_auto_20210222_1535.py index 62e9d1a7f5..ea3441b64f 100644 --- a/InvenTree/label/migrations/0006_auto_20210222_0952.py +++ b/InvenTree/label/migrations/0006_auto_20210222_1535.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.7 on 2021-02-21 22:52 +# Generated by Django 3.0.7 on 2021-02-22 04:35 import django.core.validators from django.db import migrations, models @@ -13,22 +13,22 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='stockitemlabel', - name='length', - field=models.FloatField(default=20, help_text='Label length, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Length [mm]'), + name='height', + field=models.FloatField(default=20, help_text='Label height, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Height [mm]'), ), migrations.AddField( model_name='stockitemlabel', name='width', - field=models.FloatField(default=10, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), + field=models.FloatField(default=50, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), ), migrations.AddField( model_name='stocklocationlabel', - name='length', - field=models.FloatField(default=20, help_text='Label length, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Length [mm]'), + name='height', + field=models.FloatField(default=20, help_text='Label height, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Height [mm]'), ), migrations.AddField( model_name='stocklocationlabel', name='width', - field=models.FloatField(default=10, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), + field=models.FloatField(default=50, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'), ), ] diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index ae00bc0dc1..d0962655d5 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -7,19 +7,33 @@ from __future__ import unicode_literals import os import io +import logging +import datetime -from blabel import LabelWriter - +from django.conf import settings from django.db import models from django.core.validators import FileExtensionValidator, MinValueValidator from django.core.exceptions import ValidationError, FieldError +from django.template.loader import render_to_string + from django.utils.translation import gettext_lazy as _ from InvenTree.helpers import validateFilterString, normalize +import common.models import stock.models +try: + from django_weasyprint import WeasyTemplateResponseMixin +except OSError as err: + print("OSError: {e}".format(e=err)) + print("You may require some further system packages to be installed.") + sys.exit(1) + + +logger = logging.getLogger(__name__) + def rename_label(instance, filename): """ Place the label file into the correct subdirectory """ @@ -43,6 +57,21 @@ def validate_stock_location_filters(filters): return filters +class WeasyprintLabelMixin(WeasyTemplateResponseMixin): + """ + Class for rendering a label to a PDF + """ + + pdf_filename = 'label.pdf' + pdf_attachment = True + + def __init__(self, request, template, **kwargs): + + self.request = request + self.template_name = template + self.pdf_filename = kwargs.get('filename', 'label.pdf') + + class LabelTemplate(models.Model): """ Base class for generic, filterable labels. @@ -53,6 +82,9 @@ class LabelTemplate(models.Model): # Each class of label files will be stored in a separate subdirectory SUBDIR = "label" + + # Object we will be printing against (will be filled out later) + object_to_print = None @property def template(self): @@ -92,52 +124,90 @@ class LabelTemplate(models.Model): help_text=_('Label template is enabled'), ) - length = models.FloatField( - default=20, - verbose_name=_('Length [mm]'), - help_text=_('Label length, specified in mm'), - validators=[MinValueValidator(2)] - ) - width = models.FloatField( - default=10, + default=50, verbose_name=('Width [mm]'), help_text=_('Label width, specified in mm'), validators=[MinValueValidator(2)] ) - def get_record_data(self, items): + height = models.FloatField( + default=20, + verbose_name=_('Height [mm]'), + help_text=_('Label height, specified in mm'), + validators=[MinValueValidator(2)] + ) + + @property + def template_name(self): """ - Return a list of dict objects, one for each item. + Returns the file system path to the template file. + Required for passing the file to an external process """ - return [] + template = self.label.name + template = template.replace('/', os.path.sep) + template = template.replace('\\', os.path.sep) - def render_to_file(self, filename, items, **kwargs): + template = os.path.join(settings.MEDIA_ROOT, template) + + return template + + def get_context_data(self, request): """ - Render labels to a PDF file + Supply custom context data to the template for rendering. + + Note: Override this in any subclass """ - records = self.get_record_data(items) + return {} - writer = LabelWriter(self.template) - - writer.write_labels(records, filename) - - def render(self, items, **kwargs): + def context(self, request): """ - Render labels to an in-memory PDF object, and return it + Provides context data to the template. """ - records = self.get_record_data(items) + context = self.get_context_data(request) - writer = LabelWriter(self.template) + # Add "basic" context data which gets passed to every label + context['base_url'] = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL') + context['date'] = datetime.datetime.now().date() + context['datetime'] = datetime.datetime.now() + context['request'] = request + context['user'] = request.user + context['width'] = self.width + context['height'] = self.height - buffer = io.BytesIO() + return context - writer.write_labels(records, buffer) + def render_as_string(self, request, **kwargs): + """ + Render the label to a HTML string - return buffer + Useful for debug mode (viewing generated code) + """ + + return render_to_string(self.template_name, self.context(request), request) + + def render(self, request, **kwargs): + """ + Render the label template to a PDF file + + Uses django-weasyprint plugin to render HTML template + """ + + wp = WeasyprintLabelMixin( + request, + self.template_name, + base_url=request.build_absolute_uri("/"), + presentational_hints=True, + **kwargs + ) + + return wp.render_to_response( + self.context(request), + **kwargs + ) class StockItemLabel(LabelTemplate): @@ -171,29 +241,24 @@ class StockItemLabel(LabelTemplate): return items.exists() - def get_record_data(self, items): + def get_context_data(self, request): """ Generate context data for each provided StockItem """ - records = [] - - for item in items: - # Add some basic information - records.append({ - 'item': item, - 'part': item.part, - 'name': item.part.name, - 'ipn': item.part.IPN, - 'quantity': normalize(item.quantity), - 'serial': item.serial, - 'uid': item.uid, - 'pk': item.pk, - 'qr_data': item.format_barcode(brief=True), - 'tests': item.testResultMap() - }) + stock_item = self.object_to_print - return records + return { + 'item': stock_item, + 'part': stock_item.part, + 'name': stock_item.part.full_name, + 'ipn': stock_item.part.IPN, + 'quantity': normalize(stock_item.quantity), + 'serial': stock_item.serial, + 'uid': stock_item.uid, + 'qr_data': stock_item.format_barcode(brief=True), + 'tests': stock_item.testResultMap() + } class StockLocationLabel(LabelTemplate): @@ -226,17 +291,14 @@ class StockLocationLabel(LabelTemplate): return locs.exists() - def get_record_data(self, locations): + def get_context_data(self, request): """ Generate context data for each provided StockLocation """ - records = [] - - for loc in locations: + location = self.object_to_print - records.append({ - 'location': loc, - }) - - return records + return { + 'location': location, + 'qr_data': location.format_barcode(brief=True), + } diff --git a/InvenTree/label/templates/label/label_base.html b/InvenTree/label/templates/label/label_base.html new file mode 100644 index 0000000000..2c564d1132 --- /dev/null +++ b/InvenTree/label/templates/label/label_base.html @@ -0,0 +1,28 @@ +{% load report %} +{% load barcode %} + + + + + + + {% block content %} + + {% endblock %} + diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py index dd937f0aba..09bd9fde8f 100644 --- a/InvenTree/report/api.py +++ b/InvenTree/report/api.py @@ -164,7 +164,7 @@ class ReportPrintMixin: report.object_to_print = item if debug_mode: - outputs.append(report.render_to_string(request)) + outputs.append(report.render_as_string(request)) else: outputs.append(report.render(request)) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 00777449fc..abbdea9025 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -221,7 +221,7 @@ class ReportTemplateBase(ReportBase): return context - def render_to_string(self, request, **kwargs): + def render_as_string(self, request, **kwargs): """ Render the report to a HTML stiring. From bf51e1bfb1ba2627f7adfc3ffe152f926dad9d0f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 16:39:38 +1100 Subject: [PATCH 06/26] Update label templates - New ones are automatically copied across now --- InvenTree/label/apps.py | 49 ++++++++++++++++++- .../label/templates/label/stockitem/qr.html | 20 ++++++++ .../templates/label/stocklocation/qr.html | 20 ++++++++ .../label/stocklocation/qr_and_text.html | 33 +++++++++++++ InvenTree/label/templates/stockitem/qr.html | 16 ------ .../label/templates/stocklocation/qr.html | 16 ------ .../templates/stocklocation/qr_and_text.html | 43 ---------------- 7 files changed, 120 insertions(+), 77 deletions(-) create mode 100644 InvenTree/label/templates/label/stockitem/qr.html create mode 100644 InvenTree/label/templates/label/stocklocation/qr.html create mode 100644 InvenTree/label/templates/label/stocklocation/qr_and_text.html delete mode 100644 InvenTree/label/templates/stockitem/qr.html delete mode 100644 InvenTree/label/templates/stocklocation/qr.html delete mode 100644 InvenTree/label/templates/stocklocation/qr_and_text.html diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py index 321047a551..8ec2be24f4 100644 --- a/InvenTree/label/apps.py +++ b/InvenTree/label/apps.py @@ -1,6 +1,7 @@ import os import shutil import logging +import hashlib from django.apps import AppConfig from django.conf import settings @@ -9,6 +10,20 @@ from django.conf import settings logger = logging.getLogger(__name__) +def hashFile(filename): + """ + Calculate the MD5 hash of a file + """ + + md5 = hashlib.md5() + + with open(filename, 'rb') as f: + data = f.read() + md5.update(data) + + return md5.hexdigest() + + class LabelConfig(AppConfig): name = 'label' @@ -35,6 +50,7 @@ class LabelConfig(AppConfig): src_dir = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'templates', + 'label', 'stockitem', ) @@ -70,7 +86,21 @@ class LabelConfig(AppConfig): src_file = os.path.join(src_dir, label['file']) dst_file = os.path.join(settings.MEDIA_ROOT, filename) - if not os.path.exists(dst_file): + to_copy = False + + if os.path.exists(dst_file): + # File already exists - let's see if it is the "same", + # or if we need to overwrite it with a newer copy! + + if not hashFile(dst_file) == hashFile(src_file): + logger.info(f"Hash differs for '{filename}'") + to_copy = True + + else: + logger.info(f"Label template '{filename}' is not present") + to_copy = True + + if to_copy: logger.info(f"Copying label template '{dst_file}'") shutil.copyfile(src_file, dst_file) @@ -106,6 +136,7 @@ class LabelConfig(AppConfig): src_dir = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'templates', + 'label', 'stocklocation', ) @@ -146,7 +177,21 @@ class LabelConfig(AppConfig): src_file = os.path.join(src_dir, label['file']) dst_file = os.path.join(settings.MEDIA_ROOT, filename) - if not os.path.exists(dst_file): + to_copy = False + + if os.path.exists(dst_file): + # File already exists - let's see if it is the "same", + # or if we need to overwrite it with a newer copy! + + if not hashFile(dst_file) == hashFile(src_file): + logger.info(f"Hash differs for '{filename}'") + to_copy = True + + else: + logger.info(f"Label template '{filename}' is not present") + to_copy = True + + if to_copy: logger.info(f"Copying label template '{dst_file}'") shutil.copyfile(src_file, dst_file) diff --git a/InvenTree/label/templates/label/stockitem/qr.html b/InvenTree/label/templates/label/stockitem/qr.html new file mode 100644 index 0000000000..8f489f81b8 --- /dev/null +++ b/InvenTree/label/templates/label/stockitem/qr.html @@ -0,0 +1,20 @@ +{% extends "label/label_base.html" %} + +{% load barcode %} + +{% block style %} + +.qr { + position: fixed; + left: 0mm; + top: 0mm; + height: {{ height }}mm; + width: {{ height }}mm; +} + +{% endblock %} + +{% block content %} + + +{% endblock %} diff --git a/InvenTree/label/templates/label/stocklocation/qr.html b/InvenTree/label/templates/label/stocklocation/qr.html new file mode 100644 index 0000000000..8f489f81b8 --- /dev/null +++ b/InvenTree/label/templates/label/stocklocation/qr.html @@ -0,0 +1,20 @@ +{% extends "label/label_base.html" %} + +{% load barcode %} + +{% block style %} + +.qr { + position: fixed; + left: 0mm; + top: 0mm; + height: {{ height }}mm; + width: {{ height }}mm; +} + +{% endblock %} + +{% block content %} + + +{% endblock %} diff --git a/InvenTree/label/templates/label/stocklocation/qr_and_text.html b/InvenTree/label/templates/label/stocklocation/qr_and_text.html new file mode 100644 index 0000000000..c5756b96f2 --- /dev/null +++ b/InvenTree/label/templates/label/stocklocation/qr_and_text.html @@ -0,0 +1,33 @@ +{% extends "label/label_base.html" %} + +{% load barcode %} + +{% block style %} + +.qr { + position: fixed; + left: 0mm; + top: 0mm; + height: {{ height }}mm; + width: {{ height }}mm; +} + +.loc { + font-family: Arial, Helvetica, sans-serif; + display: inline; + position: absolute; + left: {{ width }}, + top: 2mm; +} + +{% endblock %} + +{% block content %} + + + +
+ {{ location.name }} +
+ +{% endblock %} diff --git a/InvenTree/label/templates/stockitem/qr.html b/InvenTree/label/templates/stockitem/qr.html deleted file mode 100644 index 9cd9d20769..0000000000 --- a/InvenTree/label/templates/stockitem/qr.html +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/InvenTree/label/templates/stocklocation/qr.html b/InvenTree/label/templates/stocklocation/qr.html deleted file mode 100644 index 8fb22fbb40..0000000000 --- a/InvenTree/label/templates/stocklocation/qr.html +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/InvenTree/label/templates/stocklocation/qr_and_text.html b/InvenTree/label/templates/stocklocation/qr_and_text.html deleted file mode 100644 index 9a555aca15..0000000000 --- a/InvenTree/label/templates/stocklocation/qr_and_text.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - -
- {{ location.name }} -
-
-
- Location ID: {{ location.pk }} -
-
- \ No newline at end of file From 7d54997cb84001b13dcb88eb4545fd7cdd79a1d2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 16:48:00 +1100 Subject: [PATCH 07/26] Tweaks --- InvenTree/label/apps.py | 14 ++++++++++++-- .../templates/label/stocklocation/qr_and_text.html | 2 +- InvenTree/templates/js/label.js | 12 ++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py index 8ec2be24f4..9f2d3ea9c4 100644 --- a/InvenTree/label/apps.py +++ b/InvenTree/label/apps.py @@ -70,6 +70,8 @@ class LabelConfig(AppConfig): 'file': 'qr.html', 'name': 'QR Code', 'description': 'Simple QR code label', + 'width': 24, + 'height': 24, }, ] @@ -116,7 +118,9 @@ class LabelConfig(AppConfig): description=label['description'], label=filename, filters='', - enabled=True + enabled=True, + width=label['width'], + height=label['height'], ) except: pass @@ -156,11 +160,15 @@ class LabelConfig(AppConfig): 'file': 'qr.html', 'name': 'QR Code', 'description': 'Simple QR code label', + 'width': 24, + 'height': 24, }, { 'file': 'qr_and_text.html', 'name': 'QR and text', 'description': 'Label with QR code and name of location', + 'width': 50, + 'height': 24, } ] @@ -207,7 +215,9 @@ class LabelConfig(AppConfig): description=label['description'], label=filename, filters='', - enabled=True + enabled=True, + width=label['width'], + height=label['height'], ) except: pass diff --git a/InvenTree/label/templates/label/stocklocation/qr_and_text.html b/InvenTree/label/templates/label/stocklocation/qr_and_text.html index c5756b96f2..2ff54dc817 100644 --- a/InvenTree/label/templates/label/stocklocation/qr_and_text.html +++ b/InvenTree/label/templates/label/stocklocation/qr_and_text.html @@ -16,7 +16,7 @@ font-family: Arial, Helvetica, sans-serif; display: inline; position: absolute; - left: {{ width }}, + left: {{ height }}mm; top: 2mm; } diff --git a/InvenTree/templates/js/label.js b/InvenTree/templates/js/label.js index 63f028fa1d..cbb71c7b08 100644 --- a/InvenTree/templates/js/label.js +++ b/InvenTree/templates/js/label.js @@ -114,6 +114,18 @@ function selectLabel(labels, items, options={}) { * (via AJAX) from the server. */ + // If only a single label template is provided, + // just run with that! + + if (labels.length == 1) { + if (options.success) { + options.success(labels[0].pk); + } + + return; + } + + var modal = options.modal || '#modal-form'; var label_list = makeOptionsList( From 9fe9cbc795b7a110f96af6f5a3434da61ee2cab4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 16:57:33 +1100 Subject: [PATCH 08/26] Fixes weird variant display - Seems like the double import was at fault (somehow??) --- InvenTree/part/templates/part/variants.html | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/InvenTree/part/templates/part/variants.html b/InvenTree/part/templates/part/variants.html index 56aacdd1dc..e94ccf1196 100644 --- a/InvenTree/part/templates/part/variants.html +++ b/InvenTree/part/templates/part/variants.html @@ -35,18 +35,6 @@ {% endblock %} -{% block js_load %} -{{ block.super }} - - - - - - - - -{% endblock %} - {% block js_ready %} {{ block.super }} From 7582b9ccf4723aee4b2aaf91da93aa87e33db474 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 17:02:33 +1100 Subject: [PATCH 09/26] PEP fixes --- InvenTree/label/api.py | 2 -- InvenTree/label/models.py | 2 +- InvenTree/report/templatetags/barcode.py | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index 899b00b0ec..a4800cd5d9 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import sys - from django.utils.translation import ugettext as _ from django.conf.urls import url, include from django.core.exceptions import ValidationError, FieldError diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index d0962655d5..0c6a108f31 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -5,8 +5,8 @@ Label printing models # -*- coding: utf-8 -*- from __future__ import unicode_literals +import sys import os -import io import logging import datetime diff --git a/InvenTree/report/templatetags/barcode.py b/InvenTree/report/templatetags/barcode.py index 9b826d8921..e38fab1f06 100644 --- a/InvenTree/report/templatetags/barcode.py +++ b/InvenTree/report/templatetags/barcode.py @@ -2,12 +2,9 @@ Template tags for rendering various barcodes """ -import os import base64 - from io import BytesIO - from django import template import qrcode as python_qrcode From 9159afe9cefba554e04349afabd167ccfbd5bc2e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 17:27:48 +1100 Subject: [PATCH 10/26] Unit test fix --- InvenTree/stock/test_views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/stock/test_views.py b/InvenTree/stock/test_views.py index 8789e9f76e..261598ae22 100644 --- a/InvenTree/stock/test_views.py +++ b/InvenTree/stock/test_views.py @@ -105,9 +105,6 @@ class StockItemTest(StockViewTestCase): response = self.client.get(reverse('stock-item-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) - data = str(response.content) - self.assertIn('Error:', data) - def test_adjust_items(self): url = reverse('stock-adjust') From 28e9aa4afa504fd8739fc888f29524f2341952c3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 18:11:40 +1100 Subject: [PATCH 11/26] More unit fixes --- InvenTree/part/test_views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index 13349aae15..8c60941511 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -270,14 +270,13 @@ class PartQRTest(PartViewTestCase): data = str(response.content) self.assertIn('Part QR Code', data) - self.assertIn(' Date: Mon, 22 Feb 2021 18:44:31 +1100 Subject: [PATCH 12/26] Adds 'suppliers' field to Part serializer - Number of supplier parts associated with the Part object --- InvenTree/part/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 103d0202f1..1ada816357 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -245,6 +245,14 @@ class PartSerializer(InvenTreeModelSerializer): Decimal(0), ) ) + + # Annotate with the number of 'suppliers' + queryset = queryset.annotate( + suppliers=Coalesce( + SubqueryCount('supplier_parts'), + Decimal(0), + ), + ) return queryset @@ -263,6 +271,7 @@ class PartSerializer(InvenTreeModelSerializer): ordering = serializers.FloatField(read_only=True) building = serializers.FloatField(read_only=True) stock_item_count = serializers.IntegerField(read_only=True) + suppliers = serializers.IntegerField(read_only=True) image = serializers.CharField(source='get_image_url', read_only=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) @@ -308,6 +317,7 @@ class PartSerializer(InvenTreeModelSerializer): 'salable', 'starred', 'stock_item_count', + 'suppliers', 'thumbnail', 'trackable', 'units', From 199ba27031ed22a059b955ac2a42bc7f010fb4ec Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 18:44:59 +1100 Subject: [PATCH 13/26] PEP!!! --- InvenTree/part/test_views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index 8c60941511..45fd39ceb0 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -275,7 +275,6 @@ class PartQRTest(PartViewTestCase): def test_invalid_part(self): response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') - data = str(response.content) self.assertEqual(response.status_code, 200) From 2186a66465c6895a15c24070ca94f7b795936723 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 22:05:20 +1100 Subject: [PATCH 14/26] Build: Filter by parent or ancestor in API - Add unit testing --- InvenTree/build/api.py | 22 +++++ InvenTree/build/test_api.py | 163 ++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 InvenTree/build/test_api.py diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index d76c0a4b51..cb6b3f6b2b 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -56,6 +56,28 @@ class BuildList(generics.ListCreateAPIView): params = self.request.query_params + # Filter by "parent" + parent = params.get('parent', None) + + if parent is not None: + queryset = queryset.filter(parent=parent) + + # Filter by "ancestor" builds + ancestor = params.get('ancestor', None) + + if ancestor is not None: + try: + ancestor = Build.objects.get(pk=ancestor) + + descendants = ancestor.get_descendants(include_self=True) + + queryset = queryset.filter( + parent__pk__in=[b.pk for b in descendants] + ) + + except (ValueError, Build.DoesNotExist): + pass + # Filter by build status? status = params.get('status', None) diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py new file mode 100644 index 0000000000..41b09c8e08 --- /dev/null +++ b/InvenTree/build/test_api.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from datetime import datetime, timedelta + +from rest_framework.test import APITestCase +from rest_framework import status + +from django.urls import reverse +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group + +from part.models import Part +from build.models import Build + +from InvenTree.status_codes import BuildStatus + + +class BuildAPITest(APITestCase): + """ + Series of tests for the Build DRF API + """ + + fixtures = [ + 'category', + 'part', + 'location', + 'bom', + 'build', + ] + + def setUp(self): + # Create a user for auth + user = get_user_model() + + self.user = user.objects.create_user( + username='testuser', + email='test@testing.com', + password='password' + ) + + # Put the user into a group with the correct permissions + group = Group.objects.create(name='mygroup') + self.user.groups.add(group) + + # Give the group *all* the permissions! + for rule in group.rule_sets.all(): + rule.can_view = True + rule.can_change = True + rule.can_add = True + rule.can_delete = True + + rule.save() + + group.save() + + self.client.login(username='testuser', password='password') + + +class BuildListTest(BuildAPITest): + """ + Tests for the BuildOrder LIST API + """ + + url = reverse('api-build-list') + + def get(self, status=200, data={}): + + response = self.client.get(self.url, data, format='json') + + self.assertEqual(response.status_code, status) + + return response.data + + def test_get_all_builds(self): + """ + Retrieve *all* builds via the API + """ + + builds = self.get() + + self.assertEqual(len(builds), 5) + + builds = self.get(data={'active': True}) + self.assertEqual(len(builds), 1) + + builds = self.get(data={'status': BuildStatus.COMPLETE}) + self.assertEqual(len(builds), 4) + + builds = self.get(data={'overdue': False}) + self.assertEqual(len(builds), 5) + + builds = self.get(data={'overdue': True}) + self.assertEqual(len(builds), 0) + + def test_overdue(self): + """ + Create a new build, in the past + """ + + in_the_past = datetime.now().date() - timedelta(days=50) + + part = Part.objects.get(pk=50) + + build = Build.objects.create( + part=part, + quantity=10, + title='Just some thing', + status=BuildStatus.PRODUCTION, + target_date=in_the_past + ) + + builds = self.get(data={'overdue': True}) + + self.assertEqual(len(builds), 1) + + def test_sub_builds(self): + """ + Test the build / sub-build relationship + """ + + parent = Build.objects.get(pk=5) + + part = Part.objects.get(pk=50) + + n = Build.objects.count() + + # Make some sub builds + for i in range(5): + Build.objects.create( + part=part, + quantity=10, + reference=f"build-000{i}", + title=f"Sub build {i}", + parent=parent + ) + + # And some sub-sub builds + for sub_build in Build.objects.filter(parent=parent): + + for i in range(3): + Build.objects.create( + part=part, + reference=f"{sub_build.reference}-00{i}-sub", + quantity=40, + title=f"sub sub build {i}", + parent=sub_build + ) + + # 20 new builds should have been created! + self.assertEqual(Build.objects.count(), (n + 20)) + + Build.objects.rebuild() + + # Search by parent + builds = self.get(data={'parent': parent.pk}) + + self.assertEqual(len(builds), 5) + + # Search by ancestor + builds = self.get(data={'ancestor': parent.pk}) + + self.assertEqual(len(builds), 20) From c1dd5b1ca14e597753bc01ddf435f4fed44a3b2b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 22:21:46 +1100 Subject: [PATCH 15/26] Add "child build" page --- InvenTree/build/models.py | 17 +++++++++++++++++ .../build/templates/build/build_children.html | 16 ++++++++++++++++ InvenTree/build/templates/build/tabs.html | 6 ++++++ InvenTree/build/urls.py | 1 + InvenTree/build/views.py | 1 + 5 files changed, 41 insertions(+) create mode 100644 InvenTree/build/templates/build/build_children.html diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 2872fecb55..b3399ece59 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -259,6 +259,23 @@ class Build(MPTTModel): blank=True, help_text=_('Extra build notes') ) + def sub_builds(self, cascade=True): + """ + Return all Build Order objects under this one. + """ + + if cascade: + return Build.objects.filter(parent=self.pk) + else: + descendants = self.get_descendants(include_self=True) + Build.objects.filter(parent__pk__in=[d.pk for d in descendants]) + + + def sub_build_count(self, cascade=True): + + return self.sub_builds(cascade=cascade).count() + + @property def is_overdue(self): """ diff --git a/InvenTree/build/templates/build/build_children.html b/InvenTree/build/templates/build/build_children.html new file mode 100644 index 0000000000..f655a549eb --- /dev/null +++ b/InvenTree/build/templates/build/build_children.html @@ -0,0 +1,16 @@ +{% extends "build/build_base.html" %} +{% load static %} +{% load i18n %} + +{% block details %} + +{% include "build/tabs.html" with tab="children" %} + +

{% trans "Child Build Orders" %}

+ +
+ +{% endblock %} + +{% block js_ready %} +{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/tabs.html b/InvenTree/build/templates/build/tabs.html index c6d2893620..6b36c3d052 100644 --- a/InvenTree/build/templates/build/tabs.html +++ b/InvenTree/build/templates/build/tabs.html @@ -24,6 +24,12 @@ {{ build.output_count }} +
  • + + {% trans "Child Builds" %} + {{ build.sub_build_count }} + +
  • {% trans "Notes" %} diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 6f681f5488..877b368817 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -20,6 +20,7 @@ build_detail_urls = [ url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'), + url(r'^children/', views.BuildDetail.as_view(template_name='build/build_children.html'), name='build-children'), url(r'^parts/', views.BuildDetail.as_view(template_name='build/parts.html'), name='build-parts'), url(r'^attachments/', views.BuildDetail.as_view(template_name='build/attachments.html'), name='build-attachments'), url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 3c4b94c43d..272cd1e5d8 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -618,6 +618,7 @@ class BuildDetail(DetailView): ctx['bom_price'] = build.part.get_price_info(build.quantity, buy=False) ctx['BuildStatus'] = BuildStatus + ctx['sub_build_count'] = build.sub_build_count() return ctx From 58863b1924c51b7012aa1358df6405c0cfa949b2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 22 Feb 2021 22:35:54 +1100 Subject: [PATCH 16/26] Show child builds --- .../build/templates/build/build_children.html | 21 +++++++++++++++++++ InvenTree/part/templates/part/build.html | 2 +- InvenTree/templates/js/build.js | 4 +++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/InvenTree/build/templates/build/build_children.html b/InvenTree/build/templates/build/build_children.html index f655a549eb..87c8bd8608 100644 --- a/InvenTree/build/templates/build/build_children.html +++ b/InvenTree/build/templates/build/build_children.html @@ -10,7 +10,28 @@
    +
    +
    +
    + +
    +
    + +
    + +
    + {% endblock %} {% block js_ready %} + +loadBuildTable($('#sub-build-table'), { + url: '{% url "api-build-list" %}', + filterTarget: "#filter-list-sub-build", + params: { + part_detail: true, + ancestor: {{ build.pk }}, + } +}); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index 9641181af8..46685ad064 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -9,7 +9,7 @@
    -
    +
    {% if part.active %} {% if roles.build.add %} diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index cb7e27486b..01d5fcdef0 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -618,7 +618,9 @@ function loadBuildTable(table, options) { filters[key] = params[key]; } - setupFilterList("build", table); + var filterTarget = options.filterTarget || null; + + setupFilterList("build", table, filterTarget); $(table).inventreeTable({ method: 'get', From 832a6ef9a25a8608fb22e718f382da4417f9db13 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Feb 2021 09:01:03 +1100 Subject: [PATCH 17/26] PEP fixes --- InvenTree/build/models.py | 8 ++++++-- InvenTree/build/templates/build/build_children.html | 2 ++ InvenTree/build/test_api.py | 7 +++---- .../templates/report/inventree_build_order_base.html | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index b3399ece59..ada9db1e08 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -269,13 +269,17 @@ class Build(MPTTModel): else: descendants = self.get_descendants(include_self=True) Build.objects.filter(parent__pk__in=[d.pk for d in descendants]) - def sub_build_count(self, cascade=True): + """ + Return the number of sub builds under this one. + + Args: + cascade: If True (defualt), include cascading builds under sub builds + """ return self.sub_builds(cascade=cascade).count() - @property def is_overdue(self): """ diff --git a/InvenTree/build/templates/build/build_children.html b/InvenTree/build/templates/build/build_children.html index 87c8bd8608..c996aaa84f 100644 --- a/InvenTree/build/templates/build/build_children.html +++ b/InvenTree/build/templates/build/build_children.html @@ -25,6 +25,8 @@ {% block js_ready %} +{{ block.super }} + loadBuildTable($('#sub-build-table'), { url: '{% url "api-build-list" %}', filterTarget: "#filter-list-sub-build", diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index 41b09c8e08..bcfd600e9e 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from datetime import datetime, timedelta from rest_framework.test import APITestCase -from rest_framework import status from django.urls import reverse from django.contrib.auth import get_user_model @@ -64,11 +63,11 @@ class BuildListTest(BuildAPITest): url = reverse('api-build-list') - def get(self, status=200, data={}): + def get(self, status_code=200, data={}): response = self.client.get(self.url, data, format='json') - self.assertEqual(response.status_code, status) + self.assertEqual(response.status_code, status_code) return response.data @@ -102,7 +101,7 @@ class BuildListTest(BuildAPITest): part = Part.objects.get(pk=50) - build = Build.objects.create( + Build.objects.create( part=part, quantity=10, title='Just some thing', diff --git a/InvenTree/report/templates/report/inventree_build_order_base.html b/InvenTree/report/templates/report/inventree_build_order_base.html index 0a4f8b3bb6..46e7544df5 100644 --- a/InvenTree/report/templates/report/inventree_build_order_base.html +++ b/InvenTree/report/templates/report/inventree_build_order_base.html @@ -2,9 +2,9 @@ {% load i18n %} {% load report %} +{% load barcode %} {% load inventree_extras %} {% load markdownify %} -{% load qr_code %} {% block page_margin %} margin: 2cm; From 041f0561168b2cb02ba017b549c3dab4d91f9f70 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Feb 2021 10:40:52 +1100 Subject: [PATCH 18/26] Display stock tab for virtual parts --- InvenTree/part/templates/part/tabs.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 32a62f94e9..946a0efe47 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -13,11 +13,9 @@ {% trans "Variants" %} {{ part.variants.count }} {% endif %} - {% if not part.virtual %} {% trans "Stock" %} {% decimal total_stock %} - {% endif %} {% if part.component or part.salable or part.used_in_count > 0 %} {% trans "Allocated" %} {% decimal allocated %} From 5a58e0a2982ce586f808bf380343c27be9a4158e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Feb 2021 13:16:03 +1100 Subject: [PATCH 19/26] Adds "admin" button to supplier part page --- .../company/templates/company/supplier_part_base.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/InvenTree/company/templates/company/supplier_part_base.html b/InvenTree/company/templates/company/supplier_part_base.html index fde4e745d5..b03a8944e8 100644 --- a/InvenTree/company/templates/company/supplier_part_base.html +++ b/InvenTree/company/templates/company/supplier_part_base.html @@ -17,6 +17,15 @@ src="{% static 'img/blank_image.png' %}" {% block page_data %}

    {% trans "Supplier Part" %}

    +
    +

    + {{ part.part.full_name }} + {% if user.is_staff and perms.company.change_company %} + + + + {% endif %} +

    {{ part.supplier.name }} - {{ part.SKU }}

    {% if roles.purchase_order.change %} From 39d4ddd845fd92e254c81a22e51147cdc72d2442 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Feb 2021 13:23:23 +1100 Subject: [PATCH 20/26] Enable filtering for "used in" table --- InvenTree/part/templates/part/used_in.html | 5 +++-- InvenTree/templates/js/part.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html index 686578a93a..3c8c0c2928 100644 --- a/InvenTree/part/templates/part/used_in.html +++ b/InvenTree/part/templates/part/used_in.html @@ -22,12 +22,13 @@ {% block js_ready %} {{ block.super }} - loadSimplePartTable('#used-table', + loadPartTable('#used-table', '{% url "api-part-list" %}', { params: { uses: {{ part.pk }}, - } + }, + filterTarget: '#filter-list-usedin', } ); diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index c9f59ab4eb..9c21034b28 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -325,7 +325,7 @@ function loadPartTable(table, url, options={}) { filters[key] = params[key]; } - setupFilterList("parts", $(table)); + setupFilterList("parts", $(table), options.filterTarget || null); var columns = [ { From 66e1b180e43d1877548ad73f7d4494010c437aec Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Feb 2021 13:42:59 +1100 Subject: [PATCH 21/26] Add option to stock table to disable grouping at run-time --- InvenTree/templates/js/stock.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index f1794f3664..b115b8171c 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -319,6 +319,12 @@ function loadStockTable(table, options) { } } + var grouping = true; + + if ('grouping' in options) { + grouping = options.grouping; + } + table.inventreeTable({ method: 'get', formatNoMatches: function() { @@ -333,7 +339,7 @@ function loadStockTable(table, options) { {% settings_value 'STOCK_GROUP_BY_PART' as group_by_part %} {% if group_by_part %} groupByField: options.groupByField || 'part', - groupBy: true, + groupBy: grouping, groupByFormatter: function(field, id, data) { var row = data[0]; From 949a541ee0a070abd401088e847400789c043822 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Feb 2021 13:43:26 +1100 Subject: [PATCH 22/26] Add more options to StockList api - Limit query results - Order by various fields --- InvenTree/part/api.py | 2 +- InvenTree/stock/api.py | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 04a37e5fff..12bfd2de1d 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -626,7 +626,7 @@ class PartList(generics.ListCreateAPIView): queryset = queryset.filter(pk__in=parts_need_stock) - # Limit choices + # Limit number of results limit = params.get('limit', None) if limit is not None: diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 4e532b90b7..75b0d9de3b 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -808,6 +808,19 @@ class StockList(generics.ListCreateAPIView): print("After error:", str(updated_after)) pass + # Limit number of results + limit = params.get('limit', None) + + if limit is not None: + try: + limit = int(limit) + + if limit > 0: + queryset = queryset[:limit] + + except (ValueError): + pass + # Also ensure that we pre-fecth all the related items queryset = queryset.prefetch_related( 'part', @@ -815,8 +828,6 @@ class StockList(generics.ListCreateAPIView): 'location' ) - queryset = queryset.order_by('part__name') - return queryset filter_backends = [ @@ -828,6 +839,15 @@ class StockList(generics.ListCreateAPIView): filter_fields = [ ] + ordering_fields = [ + 'part__name', + 'updated', + 'stocktake_date', + 'expiry_date', + ] + + ordering = ['part__name'] + search_fields = [ 'serial', 'batch', From 571a03043ecd8e202e30b556315071c3823a8ab9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Feb 2021 13:43:41 +1100 Subject: [PATCH 23/26] Show recently updated stock items on the index page --- InvenTree/templates/InvenTree/index.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index 687ceaf0ee..fc7d5344b1 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -125,8 +125,19 @@ loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { {% if roles.stock.view %} addHeaderTitle('{% trans "Stock" %}'); +addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock'); addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart'); addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn'); + +loadStockTable($('#table-recently-updated-stock'), { + params: { + ordering: "-updated", + limit: 10, + }, + name: 'recently-updated-stock', + grouping: false, +}); + {% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} {% if expiry %} addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times'); From 8972a51bd62449f6a0683981db3865a9d28e7856 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Feb 2021 14:12:16 +1100 Subject: [PATCH 24/26] Fixes for integer validator for inventree setting --- InvenTree/common/models.py | 62 ++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 52f38bea18..0514a081d0 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -125,6 +125,13 @@ class InvenTreeSetting(models.Model): 'validator': bool }, + 'PART_RECENT_COUNT': { + 'name': _('Recent Part Count'), + 'description': _('Number of recent parts to display on index page'), + 'default': 10, + 'validator': [int, MinValueValidator(1),] + }, + 'PART_TEMPLATE': { 'name': _('Template'), 'description': _('Parts are templates by default'), @@ -521,11 +528,18 @@ class InvenTreeSetting(models.Model): validator = InvenTreeSetting.get_setting_validator(self.key) + if self.is_bool(): + self.value = InvenTree.helpers.str2bool(self.value) + + if self.is_int(): + try: + self.value = int(self.value) + except (ValueError): + raise ValidationError(_('Must be an integer value')) + if validator is not None: self.run_validator(validator) - if self.is_bool(): - self.value = InvenTree.helpers.str2bool(self.value) def run_validator(self, validator): """ @@ -535,39 +549,39 @@ class InvenTreeSetting(models.Model): if validator is None: return - # If a list of validators is supplied, iterate through each one - if type(validator) in [list, tuple]: - for v in validator: - self.run_validator(v) - - return - - if callable(validator): - # We can accept function validators with a single argument - print("Running validator function") - validator(self.value) + value = self.value # Boolean validator - if validator == bool: + if self.is_bool(): # Value must "look like" a boolean value - if InvenTree.helpers.is_bool(self.value): + if InvenTree.helpers.is_bool(value): # Coerce into either "True" or "False" - self.value = str(InvenTree.helpers.str2bool(self.value)) + value = InvenTree.helpers.str2bool(value) else: raise ValidationError({ 'value': _('Value must be a boolean value') }) - # Integer validator - if validator == int: + # Integer validator + if self.is_int(): + try: # Coerce into an integer value - self.value = str(int(self.value)) + value = int(value) except (ValueError, TypeError): raise ValidationError({ 'value': _('Value must be an integer value'), }) + # If a list of validators is supplied, iterate through each one + if type(validator) in [list, tuple]: + for v in validator: + self.run_validator(v) + + if callable(validator): + # We can accept function validators with a single argument + validator(self.value) + def validate_unique(self, exclude=None): """ Ensure that the key:value pair is unique. In addition to the base validators, this ensures that the 'key' @@ -597,7 +611,13 @@ class InvenTreeSetting(models.Model): validator = InvenTreeSetting.get_setting_validator(self.key) - return validator == bool + if validator == bool: + return True + + if type(validator) in [list, tuple]: + for v in validator: + if v == bool: + return True def as_bool(self): """ @@ -623,6 +643,8 @@ class InvenTreeSetting(models.Model): if v == int: return True + return False + def as_int(self): """ Return the value of this setting converted to a boolean value. From 8717be66fc0e776b0b0b6f7add23e09447aaa393 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Feb 2021 14:15:12 +1100 Subject: [PATCH 25/26] Configure number of "recent items" shown on index page --- InvenTree/common/models.py | 7 +++++++ InvenTree/templates/InvenTree/index.html | 4 ++-- InvenTree/templates/InvenTree/settings/part.html | 1 + InvenTree/templates/InvenTree/settings/stock.html | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 0514a081d0..40fde5dcb4 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -256,6 +256,13 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'STOCK_RECENT_COUNT': { + 'name': _('Recent Stock Count'), + 'description': _('Number of recent stock items to display on index page'), + 'default': 10, + 'validator': [int, MinValueValidator(1),] + }, + 'BUILDORDER_REFERENCE_PREFIX': { 'name': _('Build Order Reference Prefix'), 'description': _('Prefix value for build order reference'), diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index fc7d5344b1..cb2cda0a30 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -102,7 +102,7 @@ addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-ti loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", { params: { ordering: "-creation_date", - limit: 10, + limit: {% settings_value "PART_RECENT_COUNT" %}, }, name: 'latest_parts', }); @@ -132,7 +132,7 @@ addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa loadStockTable($('#table-recently-updated-stock'), { params: { ordering: "-updated", - limit: 10, + limit: {% settings_value "STOCK_RECENT_COUNT" %}, }, name: 'recently-updated-stock', grouping: false, diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index 9174b2f127..bef951e203 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -19,6 +19,7 @@ {% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %} {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" %} + {% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" %} {% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %} {% include "InvenTree/settings/setting.html" with key="PART_ASSEMBLY" icon="fa-tools" %} diff --git a/InvenTree/templates/InvenTree/settings/stock.html b/InvenTree/templates/InvenTree/settings/stock.html index 9c82202a02..7909e11a60 100644 --- a/InvenTree/templates/InvenTree/settings/stock.html +++ b/InvenTree/templates/InvenTree/settings/stock.html @@ -16,6 +16,7 @@ {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="STOCK_GROUP_BY_PART" icon="fa-layer-group" %} + {% include "InvenTree/settings/setting.html" with key="STOCK_RECENT_COUNT" icon="fa-clock" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ENABLE_EXPIRY" icon="fa-stopwatch" %} {% include "InvenTree/settings/setting.html" with key="STOCK_STALE_DAYS" icon="fa-calendar" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_SALE" icon="fa-truck" %} From 648d322f540b85b531e6889aaff8421abf6babf7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Feb 2021 14:24:09 +1100 Subject: [PATCH 26/26] Style fixes --- InvenTree/common/models.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 40fde5dcb4..06c06bde05 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -129,7 +129,7 @@ class InvenTreeSetting(models.Model): 'name': _('Recent Part Count'), 'description': _('Number of recent parts to display on index page'), 'default': 10, - 'validator': [int, MinValueValidator(1),] + 'validator': [int, MinValueValidator(1)] }, 'PART_TEMPLATE': { @@ -260,7 +260,7 @@ class InvenTreeSetting(models.Model): 'name': _('Recent Stock Count'), 'description': _('Number of recent stock items to display on index page'), 'default': 10, - 'validator': [int, MinValueValidator(1),] + 'validator': [int, MinValueValidator(1)] }, 'BUILDORDER_REFERENCE_PREFIX': { @@ -547,7 +547,6 @@ class InvenTreeSetting(models.Model): if validator is not None: self.run_validator(validator) - def run_validator(self, validator): """ Run a validator against the 'value' field for this InvenTreeSetting object. @@ -569,7 +568,7 @@ class InvenTreeSetting(models.Model): 'value': _('Value must be a boolean value') }) - # Integer validator + # Integer validator if self.is_int(): try: