Merge branch 'master' into feature-non-int-serial

This commit is contained in:
Ben Charlton 2020-08-24 11:12:07 +01:00
commit 471ece136e
14 changed files with 172 additions and 176 deletions

View File

@ -8,7 +8,7 @@ from .models import StockItemLabel
class StockItemLabelAdmin(admin.ModelAdmin): class StockItemLabelAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'label') list_display = ('name', 'description', 'label', 'filters', 'enabled')
admin.site.register(StockItemLabel, StockItemLabelAdmin) admin.site.register(StockItemLabel, StockItemLabelAdmin)

View File

@ -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'),
),
]

View File

@ -70,6 +70,12 @@ class LabelTemplate(models.Model):
validators=[validateFilterString] validators=[validateFilterString]
) )
enabled = models.BooleanField(
default=True,
help_text=_('Label template is enabled'),
verbose_name=_('Enabled')
)
def get_record_data(self, items): def get_record_data(self, items):
""" """
Return a list of dict objects, one for each item. Return a list of dict objects, one for each item.

View File

@ -43,7 +43,6 @@ from InvenTree.helpers import decimal2string, normalize
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
from report import models as ReportModels
from build import models as BuildModels from build import models as BuildModels
from order import models as OrderModels from order import models as OrderModels
from company.models import SupplierPart from company.models import SupplierPart
@ -406,24 +405,6 @@ class Part(MPTTModel):
self.category = category self.category = category
self.save() 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): def get_absolute_url(self):
""" Return the web URL for viewing this part """ """ Return the web URL for viewing this part """
return reverse('part-detail', kwargs={'pk': self.id}) return reverse('part-detail', kwargs={'pk': self.id})

View File

@ -3,13 +3,12 @@ from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import ReportTemplate, ReportAsset from .models import TestReport, ReportAsset
from .models import TestReport
class ReportTemplateAdmin(admin.ModelAdmin): class ReportTemplateAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'template') list_display = ('name', 'description', 'template', 'filters', 'enabled')
class ReportAssetAdmin(admin.ModelAdmin): class ReportAssetAdmin(admin.ModelAdmin):
@ -17,6 +16,5 @@ class ReportAssetAdmin(admin.ModelAdmin):
list_display = ('asset', 'description') list_display = ('asset', 'description')
admin.site.register(ReportTemplate, ReportTemplateAdmin)
admin.site.register(TestReport, ReportTemplateAdmin) admin.site.register(TestReport, ReportTemplateAdmin)
admin.site.register(ReportAsset, ReportAssetAdmin) admin.site.register(ReportAsset, ReportAssetAdmin)

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -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',
),
]

View File

@ -16,9 +16,11 @@ from django.conf import settings
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator
from django.core.exceptions import ValidationError 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: try:
from django_weasyprint import WeasyTemplateResponseMixin from django_weasyprint import WeasyTemplateResponseMixin
@ -55,59 +57,6 @@ def rename_template(instance, filename):
return os.path.join('report', 'report_template', instance.getSubdir(), 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 WeasyprintReportMixin(WeasyTemplateResponseMixin):
""" """
Class for rendering a HTML template to a PDF. 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")) description = models.CharField(max_length=250, help_text=_("Report template description"))
class Meta: enabled = models.BooleanField(
abstract = True default=True,
help_text=_('Report template is enabled'),
verbose_name=_('Enabled')
)
filters = models.CharField(
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(
blank=True, blank=True,
max_length=250, max_length=250,
help_text=_("Part query filters (comma-separated list of key=value pairs)"), help_text=_("Part query filters (comma-separated list of key=value pairs)"),
validators=[validateFilterString] validators=[validateFilterString]
) )
class Meta:
abstract = True
class TestReport(ReportTemplateBase, PartFilterMixin):
class TestReport(ReportTemplateBase):
""" """
Render a TestReport against a StockItem object. 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 # Requires a stock_item object to be given to it before rendering
stock_item = None 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): def get_context_data(self, request):
return { return {
'stock_item': self.stock_item, 'stock_item': self.stock_item,

View File

@ -15,6 +15,8 @@ from InvenTree.helpers import GetExportFormats
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import RoundingDecimalFormField
from report.models import TestReport
from .models import StockLocation, StockItem, StockItemTracking from .models import StockLocation, StockItem, StockItemTracking
from .models import StockItemAttachment from .models import StockItemAttachment
from .models import StockItemTestResult from .models import StockItemTestResult
@ -225,12 +227,17 @@ class TestReportFormatForm(HelperForm):
self.fields['template'].choices = self.get_template_choices() self.fields['template'].choices = self.get_template_choices()
def get_template_choices(self): def get_template_choices(self):
""" Available choices """ """
Generate a list of of TestReport options for the StockItem
"""
choices = [] choices = []
for report in self.stock_item.part.get_test_report_templates(): templates = TestReport.objects.filter(enabled=True)
choices.append((report.pk, report))
for template in templates:
if template.matches_stock_item(self.stock_item):
choices.append(template)
return choices return choices

View File

@ -124,11 +124,9 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
{% if item.part.has_test_report_templates %}
<button type='button' class='btn btn-default' id='stock-test-report' title='{% trans "Generate test report" %}'> <button type='button' class='btn btn-default' id='stock-test-report' title='{% trans "Generate test report" %}'>
<span class='fas fa-file-invoice'/> <span class='fas fa-file-invoice'/>
</button> </button>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
@ -303,7 +301,6 @@ $("#stock-serialize").click(function() {
); );
}); });
{% if item.part.has_test_report_templates %}
$("#stock-test-report").click(function() { $("#stock-test-report").click(function() {
launchModalForm( launchModalForm(
"{% url 'stock-item-test-report-select' item.id %}", "{% url 'stock-item-test-report-select' item.id %}",
@ -312,7 +309,6 @@ $("#stock-test-report").click(function() {
} }
); );
}); });
{% endif %}
$("#print-label").click(function() { $("#print-label").click(function() {
launchModalForm( launchModalForm(

View File

@ -17,9 +17,7 @@
<button type='button' class='btn btn-danger' id='delete-test-results'>{% trans "Delete Test Data" %}</button> <button type='button' class='btn btn-danger' id='delete-test-results'>{% trans "Delete Test Data" %}</button>
{% endif %} {% endif %}
<button type='button' class='btn btn-success' id='add-test-result'>{% trans "Add Test Data" %}</button> <button type='button' class='btn btn-success' id='add-test-result'>{% trans "Add Test Data" %}</button>
{% if item.part.has_test_report_templates %}
<button type='button' class='btn btn-default' id='test-report'>{% trans "Test Report" %} <span class='fas fa-tasks'></span></button> <button type='button' class='btn btn-default' id='test-report'>{% trans "Test Report" %} <span class='fas fa-tasks'></span></button>
{% endif %}
</div> </div>
<div class='filter-list' id='filter-list-stocktests'> <div class='filter-list' id='filter-list-stocktests'>
<!-- Empty div --> <!-- Empty div -->

View File

@ -310,7 +310,8 @@ class StockItemSelectLabels(AjaxView):
labels = [] labels = []
for label in StockItemLabel.objects.all(): # Construct a list of StockItemLabel objects which are enabled, and the filters match the selected StockItem
for label in StockItemLabel.objects.filter(enabled=True):
if label.matches_stock_item(item): if label.matches_stock_item(item):
labels.append(label) labels.append(label)

View File

@ -1,13 +1,21 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
<nav class="navbar navbar-xs navbar-default navbar-fixed-top ">
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid"> <div class="container-fluid">
<div class="navbar-header clearfix content-heading"> <div class="navbar-header clearfix content-heading">
<a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% static 'img/inventree.png' %}" width="32" height="32" style="display:block; margin: auto;"/></a> <a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% static 'img/inventree.png' %}" width="32" height="32" style="display:block; margin: auto;"/></a>
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div> </div>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li><a href="{% url 'part-index' %}"><span class='fas fa-shapes icon-header'></span> {% trans "Parts" %}</a></li> <li><a href="{% url 'part-index' %}"><span class='fas fa-shapes icon-header'></span>{% trans "Parts" %}</a></li>
<li><a href="{% url 'stock-index' %}"><span class='fas fa-boxes icon-header'></span>{% trans "Stock" %}</a></li> <li><a href="{% url 'stock-index' %}"><span class='fas fa-boxes icon-header'></span>{% trans "Stock" %}</a></li>
<li><a href="{% url 'build-index' %}"><span class='fas fa-tools icon-header'></span>{% trans "Build" %}</a></li> <li><a href="{% url 'build-index' %}"><span class='fas fa-tools icon-header'></span>{% trans "Build" %}</a></li>
<li class='nav navbar-nav'> <li class='nav navbar-nav'>
@ -53,4 +61,5 @@
</li> </li>
</ul> </ul>
</div> </div>
</div>
</nav> </nav>