Merge remote-tracking branch 'inventree/master' into 0.4.x

This commit is contained in:
Oliver 2021-08-09 09:45:13 +10:00
commit e8d4e2a7e6
39 changed files with 35386 additions and 32091 deletions

View File

@ -13,6 +13,9 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@v2
- name: Check Release tag
run: |
python3 ci/check_version_number.py ${{ github.event.release.tag_name }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx

View File

@ -344,13 +344,15 @@ def GetExportFormats():
]
def DownloadFile(data, filename, content_type='application/text'):
""" Create a dynamic file for the user to download.
def DownloadFile(data, filename, content_type='application/text', inline=False):
"""
Create a dynamic file for the user to download.
Args:
data: Raw file data (string or bytes)
filename: Filename for the file download
content_type: Content type for the download
inline: Download "inline" or as attachment? (Default = attachment)
Return:
A StreamingHttpResponse object wrapping the supplied data
@ -365,7 +367,10 @@ def DownloadFile(data, filename, content_type='application/text'):
response = StreamingHttpResponse(wrapper, content_type=content_type)
response['Content-Length'] = len(data)
response['Content-Disposition'] = 'attachment; filename={f}'.format(f=filename)
disposition = "inline" if inline else "attachment"
response['Content-Disposition'] = f'{disposition}; filename={filename}'
return response

View File

@ -926,6 +926,20 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'validator': bool,
},
"LABEL_INLINE": {
'name': _('Inline label display'),
'description': _('Display PDF labels in the browser, instead of downloading as a file'),
'default': True,
'validator': bool,
},
"REPORT_INLINE": {
'name': _('Inline report display'),
'description': _('Display PDF reports in the browser, instead of downloading as a file'),
'default': False,
'validator': bool,
},
'SEARCH_PREVIEW_RESULTS': {
'name': _('Search Preview Results'),
'description': _('Number of results to show in search preview window'),
@ -965,7 +979,10 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
@classmethod
def get_filters(cls, key, **kwargs):
return {'key__iexact': key, 'user__id': kwargs['user'].id}
return {
'key__iexact': key,
'user__id': kwargs['user'].id
}
class PriceBreak(models.Model):

View File

@ -109,10 +109,13 @@ class LabelPrintMixin:
else:
pdf = outputs[0].get_document().write_pdf()
inline = common.models.InvenTreeUserSetting.get_setting('LABEL_INLINE', user=request.user)
return InvenTree.helpers.DownloadFile(
pdf,
label_name,
content_type='application/pdf'
content_type='application/pdf',
inline=inline
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.4 on 2021-08-07 11:40
from django.db import migrations, models
import part.models
class Migration(migrations.Migration):
dependencies = [
('part', '0070_alter_part_variant_of'),
]
operations = [
migrations.AlterField(
model_name='partparametertemplate',
name='name',
field=models.CharField(help_text='Parameter Name', max_length=100, unique=True, validators=[part.models.validate_template_name], verbose_name='Name'),
),
]

View File

@ -2143,6 +2143,16 @@ class PartTestTemplate(models.Model):
)
def validate_template_name(name):
"""
Prevent illegal characters in "name" field for PartParameterTemplate
"""
for c in "!@#$%^&*()<>{}[].,?/\|~`_+-=\'\"":
if c in str(name):
raise ValidationError(_(f"Illegal character in template name ({c})"))
class PartParameterTemplate(models.Model):
"""
A PartParameterTemplate provides a template for key:value pairs for extra
@ -2181,7 +2191,15 @@ class PartParameterTemplate(models.Model):
except PartParameterTemplate.DoesNotExist:
pass
name = models.CharField(max_length=100, verbose_name=_('Name'), help_text=_('Parameter Name'), unique=True)
name = models.CharField(
max_length=100,
verbose_name=_('Name'),
help_text=_('Parameter Name'),
unique=True,
validators=[
validate_template_name,
]
)
units = models.CharField(max_length=25, verbose_name=_('Units'), help_text=_('Parameter Units'), blank=True)

View File

@ -204,6 +204,7 @@ def settings_value(key, *args, **kwargs):
if 'user' in kwargs:
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
return InvenTreeSetting.get_setting(key)

View File

@ -254,10 +254,13 @@ class ReportPrintMixin:
else:
pdf = outputs[0].get_document().write_pdf()
inline = common.models.InvenTreeUserSetting.get_setting('REPORT_INLINE', user=request.user)
return InvenTree.helpers.DownloadFile(
pdf,
report_name,
content_type='application/pdf'
content_type='application/pdf',
inline=inline,
)

View File

@ -30,6 +30,18 @@
</a>
</li>
<li class='list-group-item' title='{% trans "Labels" %}'>
<a href='#' class='nav-toggle' id='select-user-labels'>
<span class='fas fa-tag'></span> {% trans "Labels" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Reports" %}'>
<a href='#' class='nav-toggle' id='select-user-reports'>
<span class='fas fa-file-pdf'></span> {% trans "Reports" %}
</a>
</li>
<!--
<li class='list-group-item' title='{% trans "Settings" %}'>
<a href='#' class='nav-toggle' id='select-user-settings'>

View File

@ -68,80 +68,3 @@
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$("#param-table").inventreeTable({
url: "{% url 'api-part-parameter-template-list' %}",
queryParams: {
ordering: 'name',
},
formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; },
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'name',
title: 'Name',
sortable: 'true',
},
{
field: 'units',
title: 'Units',
sortable: 'true',
},
{
formatter: function(value, row, index, field) {
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
return html;
}
}
]
});
$("#new-param").click(function() {
launchModalForm("{% url 'part-param-template-create' %}", {
success: function() {
$("#param-table").bootstrapTable('refresh');
},
});
});
$("#param-table").on('click', '.template-edit', function() {
var button = $(this);
var url = "/part/parameter/template/" + button.attr('pk') + "/edit/";
launchModalForm(url, {
success: function() {
$("#param-table").bootstrapTable('refresh');
}
});
});
$("#param-table").on('click', '.template-delete', function() {
var button = $(this);
var url = "/part/parameter/template/" + button.attr('pk') + "/delete/";
launchModalForm(url, {
success: function() {
$("#param-table").bootstrapTable('refresh');
}
});
});
$("#import-part").click(function() {
launchModalForm("{% url 'api-part-import' %}?reset", {});
});
{% endblock %}

View File

@ -18,6 +18,8 @@
{% include "InvenTree/settings/user_settings.html" %}
{% include "InvenTree/settings/user_homepage.html" %}
{% include "InvenTree/settings/user_search.html" %}
{% include "InvenTree/settings/user_labels.html" %}
{% include "InvenTree/settings/user_reports.html" %}
{% if user.is_staff %}
@ -241,6 +243,79 @@ $("#cat-param-table").on('click', '.template-delete', function() {
});
});
$("#param-table").inventreeTable({
url: "{% url 'api-part-parameter-template-list' %}",
queryParams: {
ordering: 'name',
},
formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; },
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'name',
title: 'Name',
sortable: 'true',
},
{
field: 'units',
title: 'Units',
sortable: 'true',
},
{
formatter: function(value, row, index, field) {
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
return html;
}
}
]
});
$("#new-param").click(function() {
launchModalForm("{% url 'part-param-template-create' %}", {
success: function() {
$("#param-table").bootstrapTable('refresh');
},
});
});
$("#param-table").on('click', '.template-edit', function() {
var button = $(this);
var url = "/part/parameter/template/" + button.attr('pk') + "/edit/";
launchModalForm(url, {
success: function() {
$("#param-table").bootstrapTable('refresh');
}
});
});
$("#param-table").on('click', '.template-delete', function() {
var button = $(this);
var url = "/part/parameter/template/" + button.attr('pk') + "/delete/";
launchModalForm(url, {
success: function() {
$("#param-table").bootstrapTable('refresh');
}
});
});
$("#import-part").click(function() {
launchModalForm("{% url 'api-part-import' %}?reset", {});
});
enableNavbar({
label: 'settings',
toggleId: '#item-menu-toggle',

View File

@ -0,0 +1,23 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}user-labels{% endblock %}
{% block heading %}
{% trans "Label Settings" %}
{% endblock %}
{% block content %}
<div class='row'>
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="LABEL_INLINE" icon='fa-tag' user_setting=True %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}user-reports{% endblock %}
{% block heading %}
{% trans "Report Settings" %}
{% endblock %}
{% block content %}
<div class='row'>
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="REPORT_INLINE" icon='fa-file-pdf' user_setting=True %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -30,6 +30,17 @@ function createManufacturerPart(options={}) {
fields.manufacturer.value = options.manufacturer;
}
fields.manufacturer.secondary = {
title: '{% trans "Add Manufacturer" %}',
fields: function(data) {
var company_fields = companyFormFields();
company_fields.is_manufacturer.value = true;
return company_fields;
}
}
constructForm('{% url "api-manufacturer-part-list" %}', {
fields: fields,
method: 'POST',
@ -72,7 +83,7 @@ function supplierPartFields() {
filters: {
part_detail: true,
manufacturer_detail: true,
}
},
},
description: {},
link: {
@ -108,6 +119,33 @@ function createSupplierPart(options={}) {
fields.manufacturer_part.value = options.manufacturer_part;
}
// Add a secondary modal for the supplier
fields.supplier.secondary = {
title: '{% trans "Add Supplier" %}',
fields: function(data) {
var company_fields = companyFormFields();
company_fields.is_supplier.value = true;
return company_fields;
}
};
// Add a secondary modal for the manufacturer part
fields.manufacturer_part.secondary = {
title: '{% trans "Add Manufacturer Part" %}',
fields: function(data) {
var mp_fields = manufacturerPartFields();
if (data.part) {
mp_fields.part.value = data.part;
mp_fields.part.hidden = true;
}
return mp_fields;
}
};
constructForm('{% url "api-supplier-part-list" %}', {
fields: fields,
method: 'POST',

View File

@ -564,6 +564,30 @@ function insertConfirmButton(options) {
}
/*
* Extract all specified form values as a single object
*/
function extractFormData(fields, options) {
var data = {};
for (var idx = 0; idx < options.field_names.length; idx++) {
var name = options.field_names[idx];
var field = fields[name] || null;
if (!field) continue;
if (field.type == 'candy') continue;
data[name] = getFormFieldValue(name, field, options);
}
return data;
}
/*
* Submit form data to the server.
*
@ -950,10 +974,10 @@ function initializeRelatedFields(fields, options) {
switch (field.type) {
case 'related field':
initializeRelatedField(name, field, options);
initializeRelatedField(field, fields, options);
break;
case 'choice':
initializeChoiceField(name, field, options);
initializeChoiceField(field, fields, options);
break;
}
}
@ -968,7 +992,9 @@ function initializeRelatedFields(fields, options) {
* - field: The field data object
* - options: The options object provided by the client
*/
function addSecondaryModal(name, field, options) {
function addSecondaryModal(field, fields, options) {
var name = field.name;
var secondary = field.secondary;
@ -981,22 +1007,42 @@ function addSecondaryModal(name, field, options) {
$(options.modal).find(`label[for="id_${name}"]`).append(html);
// TODO: Launch a callback
// Callback function when the secondary button is pressed
$(options.modal).find(`#btn-new-${name}`).click(function() {
if (secondary.callback) {
// A "custom" callback can be specified for the button
secondary.callback(field, options);
} else if (secondary.api_url) {
// By default, a new modal form is created, with the parameters specified
// The parameters match the "normal" form creation parameters
// Determine the API query URL
var url = secondary.api_url || field.api_url;
secondary.onSuccess = function(data, opts) {
setRelatedFieldData(name, data, options);
};
// If the "fields" attribute is a function, call it with data
if (secondary.fields instanceof Function) {
constructForm(secondary.api_url, secondary);
// Extract form values at time of button press
var data = extractFormData(fields, options)
secondary.fields = secondary.fields(data);
}
// If no onSuccess function is defined, provide a default one
if (!secondary.onSuccess) {
secondary.onSuccess = function(data, opts) {
// Force refresh from the API, to get full detail
inventreeGet(`${url}${data.pk}/`, {}, {
success: function(responseData) {
setRelatedFieldData(name, responseData, options);
}
});
};
}
// Method should be "POST" for creation
secondary.method = secondary.method || 'POST';
constructForm(
url,
secondary
);
});
}
@ -1010,7 +1056,9 @@ function addSecondaryModal(name, field, options) {
* - field: Field definition from the OPTIONS request
* - options: Original options object provided by the client
*/
function initializeRelatedField(name, field, options) {
function initializeRelatedField(field, fields, options) {
var name = field.name;
if (!field.api_url) {
// TODO: Provide manual api_url option?
@ -1023,7 +1071,7 @@ function initializeRelatedField(name, field, options) {
// Add a button to launch a 'secondary' modal
if (field.secondary != null) {
addSecondaryModal(name, field, options);
addSecondaryModal(field, fields, options);
}
// TODO: Add 'placeholder' support for entry select2 fields
@ -1192,7 +1240,9 @@ function setRelatedFieldData(name, data, options) {
}
function initializeChoiceField(name, field, options) {
function initializeChoiceField(field, fields, options) {
var name = field.name;
var select = $(options.modal).find(`#id_${name}`);

View File

@ -13,6 +13,16 @@ function createSalesOrder(options={}) {
},
customer: {
value: options.customer,
secondary: {
title: '{% trans "Add Customer" %}',
fields: function(data) {
var fields = companyFormFields();
fields.is_customer.value = true;
return fields;
}
}
},
customer_reference: {},
description: {},
@ -44,6 +54,16 @@ function createPurchaseOrder(options={}) {
},
supplier: {
value: options.supplier,
secondary: {
title: '{% trans "Add Supplier" %}',
fields: function(data) {
var fields = companyFormFields();
fields.is_supplier.value = true;
return fields;
}
}
},
supplier_reference: {},
description: {},

View File

@ -17,7 +17,16 @@ function yesNoLabel(value) {
function partFields(options={}) {
var fields = {
category: {},
category: {
secondary: {
title: '{% trans "Add Part Category" %}',
fields: function(data) {
var fields = categoryFields();
return fields;
}
}
},
name: {},
IPN: {},
revision: {},
@ -30,7 +39,8 @@ function partFields(options={}) {
link: {
icon: 'fa-link',
},
default_location: {},
default_location: {
},
default_supplier: {},
default_expiry: {
icon: 'fa-calendar-alt',

View File

@ -2,6 +2,18 @@
{% load inventree_extras %}
{% load status_codes %}
function locationFields() {
return {
parent: {
help_text: '{% trans "Parent stock location" %}',
},
name: {},
description: {},
};
}
/* Stock API functions
* Requires api.js to be loaded first
*/

View File

@ -24,7 +24,7 @@ However, powerful business logic works in the background to ensure that stock tr
InvenTree is [available via Docker](https://hub.docker.com/r/inventree/inventree). Read the [docker guide](https://inventree.readthedocs.io/en/latest/start/docker/) for full details.
# Companion App
# Mobile App
InvenTree is supported by a [companion mobile app](https://inventree.readthedocs.io/en/latest/app/app/) which allows users access to stock control information and functionality.

View File

@ -0,0 +1,38 @@
"""
On release, ensure that the release tag matches the InvenTree version number!
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import sys
import re
import os
import argparse
if __name__ == '__main__':
here = os.path.abspath(os.path.dirname(__file__))
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
with open(version_file, 'r') as f:
results = re.findall(r'INVENTREE_SW_VERSION = "(.*)"', f.read())
if not len(results) == 1:
print(f"Could not find INVENTREE_SW_VERSION in {version_file}")
sys.exit(1)
version = results[0]
parser = argparse.ArgumentParser()
parser.add_argument('tag', help='Version tag', action='store')
args = parser.parse_args()
if not args.tag == version:
print(f"Release tag '{args.tag}' does not match INVENTREE_SW_VERSION '{version}'")
sys.exit(1)
sys.exit(0)