diff --git a/.travis.yml b/.travis.yml index bdc0aad06c..d3ac736a7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,16 +12,17 @@ addons: before_install: - sudo apt-get update - sudo apt-get install gettext - - make install - - make migrate + - pip3 install invoke + - invoke install + - invoke migrate - cd InvenTree && python3 manage.py createsuperuser --username InvenTreeAdmin --email admin@inventree.com --noinput && cd .. script: - cd InvenTree && python3 manage.py makemigrations && cd .. - python3 ci/check_migration_files.py - - make coverage - - make translate - - make style + - invoke coverage + - invoke translate + - invoke style after_success: - coveralls \ No newline at end of file diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index d3a5ee919d..24ba9278c8 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -163,7 +163,7 @@ LOGGING = { }, } -MIDDLEWARE = [ +MIDDLEWARE = CONFIG.get('middleware', [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', @@ -173,9 +173,12 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'InvenTree.middleware.AuthRequiredMiddleware' -] +]) + +AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [ + 'django.contrib.auth.backends.ModelBackend' +]) # If the debug toolbar is enabled, add the modules if DEBUG and CONFIG.get('debug_toolbar', False): diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 1c776a6f7a..0a60e205a1 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -86,4 +86,21 @@ latex: enabled: False interpreter: pdflatex # Extra options to pass through to the LaTeX interpreter - options: '' \ No newline at end of file + options: '' + +# Permit custom authentication backends +#authentication_backends: +# - 'django.contrib.auth.backends.ModelBackend' + +# Custom middleware, sometimes needed alongside an authentication backend change. +#middleware: +# - 'django.middleware.security.SecurityMiddleware' +# - 'django.contrib.sessions.middleware.SessionMiddleware' +# - 'django.middleware.locale.LocaleMiddleware' +# - 'django.middleware.common.CommonMiddleware' +# - 'django.middleware.csrf.CsrfViewMiddleware' +# - 'corsheaders.middleware.CorsMiddleware' +# - 'django.contrib.auth.middleware.AuthenticationMiddleware' +# - 'django.contrib.messages.middleware.MessageMiddleware' +# - 'django.middleware.clickjacking.XFrameOptionsMiddleware' +# - 'InvenTree.middleware.AuthRequiredMiddleware' \ No newline at end of file diff --git a/InvenTree/label/admin.py b/InvenTree/label/admin.py index bc03e122b6..71399efabb 100644 --- a/InvenTree/label/admin.py +++ b/InvenTree/label/admin.py @@ -8,7 +8,7 @@ from .models import StockItemLabel class StockItemLabelAdmin(admin.ModelAdmin): - list_display = ('name', 'description', 'label') + list_display = ('name', 'description', 'label', 'filters', 'enabled') admin.site.register(StockItemLabel, StockItemLabelAdmin) diff --git a/InvenTree/label/migrations/0002_stockitemlabel_enabled.py b/InvenTree/label/migrations/0002_stockitemlabel_enabled.py new file mode 100644 index 0000000000..684299e184 --- /dev/null +++ b/InvenTree/label/migrations/0002_stockitemlabel_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-08-22 23:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('label', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='stockitemlabel', + name='enabled', + field=models.BooleanField(default=True, help_text='Label template is enabled', verbose_name='Enabled'), + ), + ] diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index 7d481327a9..49b07572a8 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -70,6 +70,12 @@ class LabelTemplate(models.Model): validators=[validateFilterString] ) + enabled = models.BooleanField( + default=True, + help_text=_('Label template is enabled'), + verbose_name=_('Enabled') + ) + def get_record_data(self, items): """ Return a list of dict objects, one for each item. diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 2912d04a32..568a48034f 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -51,7 +51,8 @@ class PartResource(ModelResource): report_skipped = False clean_model_instances = True exclude = [ - 'bom_checksum', 'bom_checked_by', 'bom_checked_date' + 'bom_checksum', 'bom_checked_by', 'bom_checked_date', + 'lft', 'rght', 'tree_id', 'level', ] def get_queryset(self): diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index dd126d5730..239d8fabdb 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -41,7 +41,6 @@ from InvenTree.helpers import decimal2string, normalize from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus -from report import models as ReportModels from build import models as BuildModels from order import models as OrderModels from company.models import SupplierPart @@ -399,24 +398,6 @@ class Part(MPTTModel): self.category = category self.save() - def get_test_report_templates(self): - """ - Return all the TestReport template objects which map to this Part. - """ - - templates = [] - - for report in ReportModels.TestReport.objects.all(): - if report.matches_part(self): - templates.append(report) - - return templates - - def has_test_report_templates(self): - """ Return True if this part has a TestReport defined """ - - return len(self.get_test_report_templates()) > 0 - def get_absolute_url(self): """ Return the web URL for viewing this part """ return reverse('part-detail', kwargs={'pk': self.id}) diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py index 4183e8ee83..7d6403f5d9 100644 --- a/InvenTree/report/admin.py +++ b/InvenTree/report/admin.py @@ -3,13 +3,12 @@ from __future__ import unicode_literals from django.contrib import admin -from .models import ReportTemplate, ReportAsset -from .models import TestReport +from .models import TestReport, ReportAsset class ReportTemplateAdmin(admin.ModelAdmin): - list_display = ('name', 'description', 'template') + list_display = ('name', 'description', 'template', 'filters', 'enabled') class ReportAssetAdmin(admin.ModelAdmin): @@ -17,6 +16,5 @@ class ReportAssetAdmin(admin.ModelAdmin): list_display = ('asset', 'description') -admin.site.register(ReportTemplate, ReportTemplateAdmin) admin.site.register(TestReport, ReportTemplateAdmin) admin.site.register(ReportAsset, ReportAssetAdmin) diff --git a/InvenTree/report/migrations/0002_delete_reporttemplate.py b/InvenTree/report/migrations/0002_delete_reporttemplate.py new file mode 100644 index 0000000000..7abfe72c59 --- /dev/null +++ b/InvenTree/report/migrations/0002_delete_reporttemplate.py @@ -0,0 +1,16 @@ +# Generated by Django 3.0.7 on 2020-08-22 23:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0001_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='ReportTemplate', + ), + ] diff --git a/InvenTree/report/migrations/0003_testreport_enabled.py b/InvenTree/report/migrations/0003_testreport_enabled.py new file mode 100644 index 0000000000..74de252435 --- /dev/null +++ b/InvenTree/report/migrations/0003_testreport_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-08-23 10:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0002_delete_reporttemplate'), + ] + + operations = [ + migrations.AddField( + model_name='testreport', + name='enabled', + field=models.BooleanField(default=True, help_text='Report template is enabled', verbose_name='Enabled'), + ), + ] diff --git a/InvenTree/report/migrations/0004_auto_20200823_1104.py b/InvenTree/report/migrations/0004_auto_20200823_1104.py new file mode 100644 index 0000000000..ab9e961ad9 --- /dev/null +++ b/InvenTree/report/migrations/0004_auto_20200823_1104.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-08-23 11:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0003_testreport_enabled'), + ] + + operations = [ + migrations.RenameField( + model_name='testreport', + old_name='part_filters', + new_name='filters', + ), + ] diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 697f5691d5..05507099a7 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -16,9 +16,11 @@ from django.conf import settings from django.core.validators import FileExtensionValidator from django.core.exceptions import ValidationError -from django.utils.translation import gettext_lazy as _ +from stock.models import StockItem -from part import models as PartModels +from InvenTree.helpers import validateFilterString + +from django.utils.translation import gettext_lazy as _ try: from django_weasyprint import WeasyTemplateResponseMixin @@ -55,59 +57,6 @@ def rename_template(instance, filename): return os.path.join('report', 'report_template', instance.getSubdir(), filename) -def validateFilterString(value): - """ - Validate that a provided filter string looks like a list of comma-separated key=value pairs - - These should nominally match to a valid database filter based on the model being filtered. - - e.g. "category=6, IPN=12" - e.g. "part__name=widget" - - The ReportTemplate class uses the filter string to work out which items a given report applies to. - For example, an acceptance test report template might only apply to stock items with a given IPN, - so the string could be set to: - - filters = "IPN = ACME0001" - - Returns a map of key:value pairs - """ - - # Empty results map - results = {} - - value = str(value).strip() - - if not value or len(value) == 0: - return results - - groups = value.split(',') - - for group in groups: - group = group.strip() - - pair = group.split('=') - - if not len(pair) == 2: - raise ValidationError( - "Invalid group: {g}".format(g=group) - ) - - k, v = pair - - k = k.strip() - v = v.strip() - - if not k or not v: - raise ValidationError( - "Invalid group: {g}".format(g=group) - ) - - results[k] = v - - return results - - class WeasyprintReportMixin(WeasyTemplateResponseMixin): """ Class for rendering a HTML template to a PDF. @@ -198,54 +147,24 @@ class ReportTemplateBase(models.Model): description = models.CharField(max_length=250, help_text=_("Report template description")) - class Meta: - abstract = True + enabled = models.BooleanField( + default=True, + help_text=_('Report template is enabled'), + verbose_name=_('Enabled') + ) - -class ReportTemplate(ReportTemplateBase): - """ - A simple reporting template which is used to upload template files, - which can then be used in other concrete template classes. - """ - - pass - - -class PartFilterMixin(models.Model): - """ - A model mixin used for matching a report type against a Part object. - Used to assign a report to a given part using custom filters. - """ - - class Meta: - abstract = True - - def matches_part(self, part): - """ - Test if this report matches a given part. - """ - - filters = self.get_part_filters() - - parts = PartModels.Part.objects.filter(**filters) - - parts = parts.filter(pk=part.pk) - - return parts.exists() - - def get_part_filters(self): - """ Return a map of filters to be used for Part filtering """ - return validateFilterString(self.part_filters) - - part_filters = models.CharField( + filters = models.CharField( blank=True, max_length=250, help_text=_("Part query filters (comma-separated list of key=value pairs)"), validators=[validateFilterString] ) + class Meta: + abstract = True -class TestReport(ReportTemplateBase, PartFilterMixin): + +class TestReport(ReportTemplateBase): """ Render a TestReport against a StockItem object. """ @@ -256,6 +175,17 @@ class TestReport(ReportTemplateBase, PartFilterMixin): # Requires a stock_item object to be given to it before rendering stock_item = None + def matches_stock_item(self, item): + """ + Test if this report template matches a given StockItem objects + """ + + filters = validateFilterString(self.part_filters) + + items = StockItem.objects.filter(**filters) + + return items.exists() + def get_context_data(self, request): return { 'stock_item': self.stock_item, diff --git a/InvenTree/setup.py b/InvenTree/setup.py deleted file mode 100644 index b74cfe7358..0000000000 --- a/InvenTree/setup.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Performs initial setup functions. - -- Generates a Django SECRET_KEY file to be used by manage.py -- Copies config template file (if a config file does not already exist) -""" - -import random -import string -import os -import sys -import argparse -from shutil import copyfile - -OUTPUT_DIR = os.path.dirname(os.path.realpath(__file__)) - -KEY_FN = 'secret_key.txt' -CONFIG_FN = 'config.yaml' -CONFIG_TEMPLATE_FN = 'config_template.yaml' - - -def generate_key(length=50): - """ Generate a random string - - Args: - length: Number of characters in returned string (default = 50) - - Returns: - Randomized secret key string - """ - - options = string.digits + string.ascii_letters + string.punctuation - key = ''.join([random.choice(options) for i in range(length)]) - return key - - -if __name__ == '__main__': - - parser = argparse.ArgumentParser(description='Generate Django SECRET_KEY file') - parser.add_argument('--force', '-f', help='Override existing files', action='store_true') - parser.add_argument('--dummy', '-d', help='Dummy run (do not create any files)', action='store_true') - - args = parser.parse_args() - - # Places to store files - key_filename = os.path.join(OUTPUT_DIR, KEY_FN) - conf_template = os.path.join(OUTPUT_DIR, CONFIG_TEMPLATE_FN) - conf_filename = os.path.join(OUTPUT_DIR, CONFIG_FN) - - # Generate secret key data - key_data = generate_key() - - if args.dummy: - print('SECRET_KEY: {k}'.format(k=key_data)) - sys.exit(0) - - if not args.force and os.path.exists(key_filename): - print("Key file already exists - '{f}'".format(f=key_filename)) - else: - with open(key_filename, 'w') as key_file: - print("Generating SECRET_KEY file - '{f}'".format(f=key_filename)) - key_file.write(key_data) - - if not args.force and os.path.exists(conf_filename): - print("Config file already exists (skipping)") - else: - print("Copying config template to 'config.yaml'") - copyfile(conf_template, conf_filename) diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 333c7d35c1..d3f0c31f8b 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -13,7 +13,7 @@ from .models import StockItemTracking from .models import StockItemTestResult from build.models import Build -from company.models import SupplierPart +from company.models import Company, SupplierPart from order.models import PurchaseOrder, SalesOrder from part.models import Part @@ -59,12 +59,14 @@ class StockItemResource(ModelResource): # Custom manaegrs for ForeignKey fields part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) - part_name = Field(attribute='part__full_ame', readonly=True) + part_name = Field(attribute='part__full_name', readonly=True) supplier_part = Field(attribute='supplier_part', widget=widgets.ForeignKeyWidget(SupplierPart)) supplier = Field(attribute='supplier_part__supplier__id', readonly=True) + customer = Field(attribute='customer', widget=widgets.ForeignKeyWidget(Company)) + supplier_name = Field(attribute='supplier_part__supplier__name', readonly=True) status_label = Field(attribute='status_label', readonly=True) @@ -77,6 +79,8 @@ class StockItemResource(ModelResource): build = Field(attribute='build', widget=widgets.ForeignKeyWidget(Build)) + parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(StockItem)) + sales_order = Field(attribute='sales_order', widget=widgets.ForeignKeyWidget(SalesOrder)) build_order = Field(attribute='build_order', widget=widgets.ForeignKeyWidget(Build)) @@ -101,6 +105,11 @@ class StockItemResource(ModelResource): report_skipped = False clean_model_instance = True + exclude = [ + # Exclude MPTT internal model fields + 'lft', 'rght', 'tree_id', 'level', + ] + class StockItemAdmin(ImportExportModelAdmin): diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 234da6d53b..97d028ec2b 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -15,6 +15,8 @@ from InvenTree.helpers import GetExportFormats from InvenTree.forms import HelperForm from InvenTree.fields import RoundingDecimalFormField +from report.models import TestReport + from .models import StockLocation, StockItem, StockItemTracking from .models import StockItemAttachment from .models import StockItemTestResult @@ -225,12 +227,17 @@ class TestReportFormatForm(HelperForm): self.fields['template'].choices = self.get_template_choices() def get_template_choices(self): - """ Available choices """ + """ + Generate a list of of TestReport options for the StockItem + """ choices = [] - for report in self.stock_item.part.get_test_report_templates(): - choices.append((report.pk, report)) + templates = TestReport.objects.filter(enabled=True) + + for template in templates: + if template.matches_stock_item(self.stock_item): + choices.append(template) return choices diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 1b4171297e..bcc2087b25 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -124,11 +124,9 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endif %} - {% if item.part.has_test_report_templates %} - {% endif %} {% endblock %} @@ -303,7 +301,6 @@ $("#stock-serialize").click(function() { ); }); -{% if item.part.has_test_report_templates %} $("#stock-test-report").click(function() { launchModalForm( "{% url 'stock-item-test-report-select' item.id %}", @@ -312,7 +309,6 @@ $("#stock-test-report").click(function() { } ); }); -{% endif %} $("#print-label").click(function() { launchModalForm( diff --git a/InvenTree/stock/templates/stock/item_tests.html b/InvenTree/stock/templates/stock/item_tests.html index c79068349d..10bb0950e4 100644 --- a/InvenTree/stock/templates/stock/item_tests.html +++ b/InvenTree/stock/templates/stock/item_tests.html @@ -17,9 +17,7 @@ {% endif %} - {% if item.part.has_test_report_templates %} - {% endif %}