From 6b48977e7b5d183ebeb852a06918cf7a683b88a4 Mon Sep 17 00:00:00 2001 From: eeintech <eeintech@eeinte.ch> Date: Tue, 29 Sep 2020 15:16:12 -0500 Subject: [PATCH 01/77] Added 'Parametric Table' tab to category detail view, added part_count to 'Parts' tab --- InvenTree/part/templates/part/category.html | 15 +++++++ .../templates/part/category_parametric.html | 12 +++++ .../templates/part/category_partlist.html | 12 +++++ .../part/templates/part/category_tabs.html | 11 +++++ InvenTree/part/urls.py | 3 +- InvenTree/part/views.py | 13 +++++- InvenTree/templates/js/part.html | 45 +++++++++++++++++++ 7 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 InvenTree/part/templates/part/category_parametric.html create mode 100644 InvenTree/part/templates/part/category_partlist.html create mode 100644 InvenTree/part/templates/part/category_tabs.html diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 1c7980ba9b..1f5ee7c48b 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -115,8 +115,11 @@ </div> </div> + +{% block part_list %} <table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'> </table> +{% endblock part_list %} {% endblock %} {% block js_load %} @@ -242,4 +245,16 @@ }, ); + loadParametricPartTable( + "#parametric-part-table", + "{% url 'api-part-list' %}", + { + params: { + {% if category %}category: {{ category.id }}, + {% else %}category: "null", + {% endif %} + }, + }, + ); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/category_parametric.html b/InvenTree/part/templates/part/category_parametric.html new file mode 100644 index 0000000000..7139a539b0 --- /dev/null +++ b/InvenTree/part/templates/part/category_parametric.html @@ -0,0 +1,12 @@ +{% extends "part/category.html" %} +{% load static %} +{% load i18n %} + +{% block part_list %} + +{% include 'part/category_tabs.html' with tab='parametric-table' %} + +<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='parametric-part-table'> +</table> + +{% endblock %} diff --git a/InvenTree/part/templates/part/category_partlist.html b/InvenTree/part/templates/part/category_partlist.html new file mode 100644 index 0000000000..bf4aef52a8 --- /dev/null +++ b/InvenTree/part/templates/part/category_partlist.html @@ -0,0 +1,12 @@ +{% extends "part/category.html" %} +{% load static %} +{% load i18n %} + +{% block part_list %} + +{% include 'part/category_tabs.html' with tab='part-list' %} + +<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'> +</table> + +{% endblock %} diff --git a/InvenTree/part/templates/part/category_tabs.html b/InvenTree/part/templates/part/category_tabs.html new file mode 100644 index 0000000000..b5d8d3c214 --- /dev/null +++ b/InvenTree/part/templates/part/category_tabs.html @@ -0,0 +1,11 @@ +{% load i18n %} +{% load inventree_extras %} + +<ul class="nav nav-tabs"> + <li{% ifequal tab 'part-list' %} class="active"{% endifequal %}> + <a href="{% url 'category-detail' category.id %}">{% trans "Parts" %} <span class="badge">{% decimal part_count %}</span></a> + </li> + <li{% ifequal tab 'parametric-table' %} class='active'{% endifequal %}> + <a href="{% url 'category-parametric' category.id %}">{% trans "Parametric Table" %}</a> + </li> +</ul> diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index e61947e243..88d15c1b5b 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -77,7 +77,8 @@ part_category_urls = [ url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'), url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'), - url('^.*$', views.CategoryDetail.as_view(), name='category-detail'), + url(r'^parametric/?', views.CategoryDetail.as_view(template_name='part/category_parametric.html'), name='category-parametric'), + url(r'^.*$', views.CategoryDetail.as_view(template_name='part/category_partlist.html'), name='category-detail'), ] part_bom_urls = [ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index ccf607afc0..3c94afc5a5 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1875,7 +1875,18 @@ class CategoryDetail(DetailView): model = PartCategory context_object_name = 'category' queryset = PartCategory.objects.all().prefetch_related('children') - template_name = 'part/category.html' + template_name = 'part/category_partlist.html' + + def get_context_data(self, **kwargs): + + context = super(CategoryDetail, self).get_context_data(**kwargs).copy() + + try: + context['part_count'] = kwargs['object'].partcount() + except KeyError: + context['part_count'] = 0 + + return context class CategoryEdit(AjaxUpdateView): diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index 5576d91367..0b32ef61b9 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -163,6 +163,51 @@ function loadSimplePartTable(table, url, options={}) { } +function loadParametricPartTable(table, url, options={}) { + /* Load parametric part data into specified table. + * + * Args: + * - table: HTML reference to the table + * - url: Base URL for API query + */ + + // Ensure category detail is included + options.params['category_detail'] = true; + + var params = options.params || {}; + console.log(params) + + var filters = {}; + for (var key in params) { + filters[key] = params[key]; + } + console.log(filters) + + var columns = [ + { + field: 'pk', + title: 'ID', + visible: true, + switchable: true, + searchable: false, + } + ]; + + $(table).inventreeTable({ + url: url, + sortName: 'pk', + method: 'get', + queryParams: filters, + groupBy: false, + name: options.name || 'part', + original: params, + formatNoMatches: function() { return "{% trans "No parts found" %}"; }, + columns: columns, + showColumns: true, + }); +} + + function loadPartTable(table, url, options={}) { /* Load part listing data into specified table. * From d05a5978a044662af8df7ec3a3ea6f0c0163387b Mon Sep 17 00:00:00 2001 From: eeintech <eeintech@eeinte.ch> Date: Tue, 29 Sep 2020 16:13:08 -0500 Subject: [PATCH 02/77] Unique parameters names from category makes it to bootstrap table --- InvenTree/part/models.py | 14 ++++ InvenTree/part/templates/part/category.html | 16 +--- .../templates/part/category_parametric.html | 21 ++++- .../templates/part/category_partlist.html | 2 +- InvenTree/part/urls.py | 4 +- InvenTree/part/views.py | 15 ++++ InvenTree/templates/js/part.html | 80 +++++++++++++++++-- 7 files changed, 127 insertions(+), 25 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index afb2dfa64e..707b0b948c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -111,6 +111,20 @@ class PartCategory(InvenTreeTree): """ True if there are any parts in this category """ return self.partcount() > 0 + def get_unique_parameters(self, cascade=True): + """ Get all parameters for all parts from this category """ + parameters = [] + + parts = self.get_parts(cascade=cascade).prefetch_related('parameters', 'parameters__template') + + for part in parts: + for parameter in part.parameters.all(): + template_name = parameter.template.name + if template_name not in parameters: + parameters.append(template_name) + + return parameters + @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') def before_delete_part_category(sender, instance, using, **kwargs): diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 1f5ee7c48b..0f5ba6de43 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -116,10 +116,10 @@ </div> -{% block part_list %} +{% block category_tables %} <table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'> </table> -{% endblock part_list %} +{% endblock category_tables %} {% endblock %} {% block js_load %} @@ -245,16 +245,4 @@ }, ); - loadParametricPartTable( - "#parametric-part-table", - "{% url 'api-part-list' %}", - { - params: { - {% if category %}category: {{ category.id }}, - {% else %}category: "null", - {% endif %} - }, - }, - ); - {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/category_parametric.html b/InvenTree/part/templates/part/category_parametric.html index 7139a539b0..5c99fe391b 100644 --- a/InvenTree/part/templates/part/category_parametric.html +++ b/InvenTree/part/templates/part/category_parametric.html @@ -2,7 +2,7 @@ {% load static %} {% load i18n %} -{% block part_list %} +{% block category_tables %} {% include 'part/category_tabs.html' with tab='parametric-table' %} @@ -10,3 +10,22 @@ </table> {% endblock %} + +{% block js_ready %} +{{ block.super }} + + loadParametricPartTable( + "#parametric-part-table", + "{% url 'api-part-list' %}", + { + params: { + {% if category %}category: {{ category.id }}, + {% else %}category: "null", + {% endif %} + }, + headers: {{ parameters|safe }}, + name: 'parametric', + }, + ); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/category_partlist.html b/InvenTree/part/templates/part/category_partlist.html index bf4aef52a8..302fdd3a02 100644 --- a/InvenTree/part/templates/part/category_partlist.html +++ b/InvenTree/part/templates/part/category_partlist.html @@ -2,7 +2,7 @@ {% load static %} {% load i18n %} -{% block part_list %} +{% block category_tables %} {% include 'part/category_tabs.html' with tab='part-list' %} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 88d15c1b5b..32cd1b0615 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -77,8 +77,8 @@ part_category_urls = [ url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'), url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'), - url(r'^parametric/?', views.CategoryDetail.as_view(template_name='part/category_parametric.html'), name='category-parametric'), - url(r'^.*$', views.CategoryDetail.as_view(template_name='part/category_partlist.html'), name='category-detail'), + url(r'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'), + url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'), ] part_bom_urls = [ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 3c94afc5a5..80e5beadf8 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1889,6 +1889,21 @@ class CategoryDetail(DetailView): return context +class CategoryParametric(CategoryDetail): + """ Parametric view for PartCategory """ + template_name = 'part/category_parametric.html' + + def get_context_data(self, **kwargs): + + context = super(CategoryParametric, self).get_context_data(**kwargs).copy() + + category = kwargs['object'] + context['parameters'] = category.get_unique_parameters() + print(context) + + return context + + class CategoryEdit(AjaxUpdateView): """ Update view to edit a PartCategory """ model = PartCategory diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index 0b32ef61b9..4d8d09dc9c 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -171,13 +171,10 @@ function loadParametricPartTable(table, url, options={}) { * - url: Base URL for API query */ - // Ensure category detail is included - options.params['category_detail'] = true; - var params = options.params || {}; console.log(params) - var filters = {}; + var filters = loadTableFilters("parts"); for (var key in params) { filters[key] = params[key]; } @@ -187,19 +184,88 @@ function loadParametricPartTable(table, url, options={}) { { field: 'pk', title: 'ID', - visible: true, - switchable: true, + visible: false, + switchable: false, searchable: false, } ]; + columns.push({ + field: 'IPN', + title: 'IPN', + sortable: true, + }), + + columns.push({ + field: 'name', + title: '{% trans 'Part' %}', + sortable: true, + formatter: function(value, row, index, field) { + + var name = ''; + + if (row.IPN) { + name += row.IPN; + name += ' | '; + } + + name += value; + + if (row.revision) { + name += ' | '; + name += row.revision; + } + + if (row.is_template) { + name = '<i>' + name + '</i>'; + } + + var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/'); + + if (row.is_template) { + display += `<span class='fas fa-clone label-right' title='{% trans "Template part" %}'></span>`; + } + + if (row.assembly) { + display += `<span class='fas fa-tools label-right' title='{% trans "Assembled part" %}'></span>`; + } + + if (row.starred) { + display += `<span class='fas fa-star label-right' title='{% trans "Starred part" %}'></span>`; + } + + if (row.salable) { + display += `<span class='fas fa-dollar-sign label-right' title='{% trans "Salable part" %}'></span>`; + } + + /* + if (row.component) { + display = display + `<span class='fas fa-cogs label-right' title='Component part'></span>`; + } + */ + + if (!row.active) { + display += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`; + } + return display; + } + }); + + for (header of options.headers) { + columns.push({ + field: header, + title: header, + sortable: true, + }) + } + $(table).inventreeTable({ url: url, sortName: 'pk', method: 'get', queryParams: filters, groupBy: false, - name: options.name || 'part', + name: options.name || 'parametric', original: params, formatNoMatches: function() { return "{% trans "No parts found" %}"; }, columns: columns, From 40d8a07acc8a63a505908110c961531069049e45 Mon Sep 17 00:00:00 2001 From: eeintech <eeintech@eeinte.ch> Date: Tue, 29 Sep 2020 16:49:53 -0500 Subject: [PATCH 03/77] Now loading data! Still need to be bonified --- InvenTree/part/models.py | 32 ++++-- .../templates/part/category_parametric.html | 14 +-- InvenTree/part/views.py | 6 +- InvenTree/templates/js/part.html | 98 +++---------------- 4 files changed, 47 insertions(+), 103 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 707b0b948c..41fcf0bfce 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -112,18 +112,38 @@ class PartCategory(InvenTreeTree): return self.partcount() > 0 def get_unique_parameters(self, cascade=True): - """ Get all parameters for all parts from this category """ - parameters = [] + """ Get all unique parameter names for all parts from this category """ + unique_parameters_names = [] parts = self.get_parts(cascade=cascade).prefetch_related('parameters', 'parameters__template') for part in parts: for parameter in part.parameters.all(): - template_name = parameter.template.name - if template_name not in parameters: - parameters.append(template_name) + parameter_name = parameter.template.name + if parameter_name not in unique_parameters_names: + unique_parameters_names.append(parameter_name) - return parameters + return unique_parameters_names + + def get_parts_parameters(self, cascade=True): + """ Get all parameter names and values for all parts from this category """ + category_parameters = [] + + parts = self.get_parts(cascade=cascade).prefetch_related('parameters', 'parameters__template') + + for part in parts: + part_parameters = { + 'IPN': part.IPN, + 'Name': part.name, + } + for parameter in part.parameters.all(): + parameter_name = parameter.template.name + parameter_value = parameter.data + part_parameters[parameter_name] = parameter_value + + category_parameters.append(part_parameters) + + return category_parameters @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') diff --git a/InvenTree/part/templates/part/category_parametric.html b/InvenTree/part/templates/part/category_parametric.html index 5c99fe391b..4643dfee8a 100644 --- a/InvenTree/part/templates/part/category_parametric.html +++ b/InvenTree/part/templates/part/category_parametric.html @@ -16,16 +16,10 @@ loadParametricPartTable( "#parametric-part-table", - "{% url 'api-part-list' %}", - { - params: { - {% if category %}category: {{ category.id }}, - {% else %}category: "null", - {% endif %} - }, - headers: {{ parameters|safe }}, - name: 'parametric', - }, + { + headers: {{ headers|safe }}, + data: {{ parameters|safe }}, + } ); {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 80e5beadf8..0f0f21b9fc 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1898,8 +1898,10 @@ class CategoryParametric(CategoryDetail): context = super(CategoryParametric, self).get_context_data(**kwargs).copy() category = kwargs['object'] - context['parameters'] = category.get_unique_parameters() - print(context) + context['headers'] = category.get_unique_parameters() + context['headers'].append('IPN') + context['headers'].append('Name') + context['parameters'] = category.get_parts_parameters() return context diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index 4d8d09dc9c..5ba46b4f31 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -163,95 +163,22 @@ function loadSimplePartTable(table, url, options={}) { } -function loadParametricPartTable(table, url, options={}) { +function loadParametricPartTable(table, options={}) { /* Load parametric part data into specified table. * * Args: * - table: HTML reference to the table - * - url: Base URL for API query + * - data: Parameters data */ - var params = options.params || {}; - console.log(params) + var table_headers = options.headers + var table_data = options.data +/* console.log(table_headers) + console.log(table_data)*/ - var filters = loadTableFilters("parts"); - for (var key in params) { - filters[key] = params[key]; - } - console.log(filters) + var columns = []; - var columns = [ - { - field: 'pk', - title: 'ID', - visible: false, - switchable: false, - searchable: false, - } - ]; - - columns.push({ - field: 'IPN', - title: 'IPN', - sortable: true, - }), - - columns.push({ - field: 'name', - title: '{% trans 'Part' %}', - sortable: true, - formatter: function(value, row, index, field) { - - var name = ''; - - if (row.IPN) { - name += row.IPN; - name += ' | '; - } - - name += value; - - if (row.revision) { - name += ' | '; - name += row.revision; - } - - if (row.is_template) { - name = '<i>' + name + '</i>'; - } - - var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/'); - - if (row.is_template) { - display += `<span class='fas fa-clone label-right' title='{% trans "Template part" %}'></span>`; - } - - if (row.assembly) { - display += `<span class='fas fa-tools label-right' title='{% trans "Assembled part" %}'></span>`; - } - - if (row.starred) { - display += `<span class='fas fa-star label-right' title='{% trans "Starred part" %}'></span>`; - } - - if (row.salable) { - display += `<span class='fas fa-dollar-sign label-right' title='{% trans "Salable part" %}'></span>`; - } - - /* - if (row.component) { - display = display + `<span class='fas fa-cogs label-right' title='Component part'></span>`; - } - */ - - if (!row.active) { - display += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`; - } - return display; - } - }); - - for (header of options.headers) { + for (header of table_headers) { columns.push({ field: header, title: header, @@ -260,16 +187,17 @@ function loadParametricPartTable(table, url, options={}) { } $(table).inventreeTable({ - url: url, - sortName: 'pk', +/* url: url,*/ + sortName: 'name', method: 'get', - queryParams: filters, + queryParams: table_headers, groupBy: false, name: options.name || 'parametric', - original: params, +/* original: params,*/ formatNoMatches: function() { return "{% trans "No parts found" %}"; }, columns: columns, showColumns: true, + data: table_data, }); } From 756f3ddb0fed6aacbe5283c27a4d9844a9bf347e Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Thu, 1 Oct 2020 00:25:24 +1000 Subject: [PATCH 04/77] Hide main elements of navigation bar based on user permissions --- InvenTree/templates/navbar.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index cfadad977c..57a902b755 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -15,9 +15,16 @@ </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> + {% if perms.part.view_part or perms.part.view_partcategory %} <li><a href="{% url 'part-index' %}"><span class='fas fa-shapes icon-header'></span>{% trans "Parts" %}</a></li> + {% endif %} + {% if perms.stock.view_stockitem or perms.part.view_stocklocation %} <li><a href="{% url 'stock-index' %}"><span class='fas fa-boxes icon-header'></span>{% trans "Stock" %}</a></li> + {% endif %} + {% if perms.build %} <li><a href="{% url 'build-index' %}"><span class='fas fa-tools icon-header'></span>{% trans "Build" %}</a></li> + {% endif %} + {% if perms.order.view_purchaseorder %} <li class='nav navbar-nav'> <a class='dropdown-toggle' data-toggle='dropdown' href='#'><span class='fas fa-shopping-cart icon-header'></span>{% trans "Buy" %}</a> <ul class='dropdown-menu'> @@ -26,6 +33,8 @@ <li><a href="{% url 'po-index' %}"><span class='fas fa-list icon-header'></span>{% trans "Purchase Orders" %}</a></li> </ul> </li> + {% endif %} + {% if perms.order.view_salesorder %} <li class='nav navbar-nav'> <a class='dropdown-toggle' data-toggle='dropdown' href='#'><span class='fas fa-truck icon-header'></span>{% trans "Sell" %}</a> <ul class='dropdown-menu'> @@ -33,6 +42,7 @@ <li><a href="{% url 'so-index' %}"><span class='fas fa-list icon-header'></span>{% trans "Sales Orders" %}</a></li> </ul> </li> + {% endif %} </ul> <ul class="nav navbar-nav navbar-right"> {% include "search_form.html" %} From b7d25a75c48b4f873771a9654a3d54ba7aaef92b Mon Sep 17 00:00:00 2001 From: eeintech <eeintech@eeinte.ch> Date: Thu, 1 Oct 2020 10:03:49 -0500 Subject: [PATCH 05/77] Hide part toolbar, nicer part representation, improved parameters prefetching --- InvenTree/part/models.py | 32 +++++++++++---- .../templates/part/category_parametric.html | 6 +++ InvenTree/part/views.py | 23 ++++++++--- InvenTree/templates/js/part.html | 40 +++++++++++++++---- 4 files changed, 82 insertions(+), 19 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 41fcf0bfce..272afacd0e 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -111,11 +111,20 @@ class PartCategory(InvenTreeTree): """ True if there are any parts in this category """ return self.partcount() > 0 - def get_unique_parameters(self, cascade=True): + def prefetch_parts_parameters(self, cascade=True): + """ Prefectch parts parameters """ + + return self.get_parts(cascade=cascade).prefetch_related('parameters', 'parameters__template') + + def get_unique_parameters(self, cascade=True, prefetch=None): """ Get all unique parameter names for all parts from this category """ + unique_parameters_names = [] - parts = self.get_parts(cascade=cascade).prefetch_related('parameters', 'parameters__template') + if prefetch: + parts = prefetch + else: + parts = self.prefetch_parts_parameters(cascade=cascade) for part in parts: for parameter in part.parameters.all(): @@ -123,19 +132,28 @@ class PartCategory(InvenTreeTree): if parameter_name not in unique_parameters_names: unique_parameters_names.append(parameter_name) - return unique_parameters_names + return sorted(unique_parameters_names) - def get_parts_parameters(self, cascade=True): + def get_parts_parameters(self, cascade=True, prefetch=None): """ Get all parameter names and values for all parts from this category """ + category_parameters = [] - parts = self.get_parts(cascade=cascade).prefetch_related('parameters', 'parameters__template') + if prefetch: + parts = prefetch + else: + parts = self.prefetch_parts_parameters(cascade=cascade) for part in parts: part_parameters = { - 'IPN': part.IPN, - 'Name': part.name, + 'pk': part.pk, + 'name': part.name, + 'description': part.description, } + # Add IPN only if it exists + if part.IPN: + part_parameters['IPN'] = part.IPN + for parameter in part.parameters.all(): parameter_name = parameter.template.name parameter_value = parameter.data diff --git a/InvenTree/part/templates/part/category_parametric.html b/InvenTree/part/templates/part/category_parametric.html index 4643dfee8a..3cf4f37a20 100644 --- a/InvenTree/part/templates/part/category_parametric.html +++ b/InvenTree/part/templates/part/category_parametric.html @@ -13,6 +13,12 @@ {% block js_ready %} {{ block.super }} + + /* Hide Button Toolbar */ + window.onload = function hideButtonToolbar() { + var toolbar = document.getElementById("button-toolbar"); + toolbar.style.display = "none"; + }; loadParametricPartTable( "#parametric-part-table", diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 0f0f21b9fc..dc8d07f5cd 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1872,6 +1872,7 @@ class PartParameterDelete(AjaxDeleteView): class CategoryDetail(DetailView): """ Detail view for PartCategory """ + model = PartCategory context_object_name = 'category' queryset = PartCategory.objects.all().prefetch_related('children') @@ -1891,17 +1892,29 @@ class CategoryDetail(DetailView): class CategoryParametric(CategoryDetail): """ Parametric view for PartCategory """ + template_name = 'part/category_parametric.html' def get_context_data(self, **kwargs): context = super(CategoryParametric, self).get_context_data(**kwargs).copy() - category = kwargs['object'] - context['headers'] = category.get_unique_parameters() - context['headers'].append('IPN') - context['headers'].append('Name') - context['parameters'] = category.get_parts_parameters() + # Get current category + category = kwargs.get('object', None) + + if category: + cascade = kwargs.get('cascade', True) + # Prefetch parts parameters + parts_parameters = category.prefetch_parts_parameters(cascade=cascade) + # Get table headers (unique parameters names) + context['headers'] = category.get_unique_parameters(cascade=cascade, + prefetch=parts_parameters) + # Insert part information + context['headers'].insert(0, 'description') + context['headers'].insert(0, 'part') + # Get parameters data + context['parameters'] = category.get_parts_parameters(cascade=cascade, + prefetch=parts_parameters) return context diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index 5ba46b4f31..59406e7672 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -179,17 +179,43 @@ function loadParametricPartTable(table, options={}) { var columns = []; for (header of table_headers) { - columns.push({ - field: header, - title: header, - sortable: true, - }) + if (header === 'part') { + columns.push({ + field: 'part', + title: '{% trans 'Part' %}', + sortable: true, + formatter: function(value, row, index, field) { + + var name = ''; + + if (row.IPN) { + name += row.IPN + ' | ' + row.name; + } else { + name += row.name; + } + + return renderLink(name, '/part/' + row.pk + '/'); + } + }); + } else if (header === 'description') { + columns.push({ + field: header, + title: '{% trans 'Description' %}', + sortable: true, + }); + } else { + columns.push({ + field: header, + title: header, + sortable: true, + }); + } } $(table).inventreeTable({ /* url: url,*/ - sortName: 'name', - method: 'get', + sortName: 'part', +/* method: 'get',*/ queryParams: table_headers, groupBy: false, name: options.name || 'parametric', From 15e1c0579125032bc4990915cc29bfa853a5a40f Mon Sep 17 00:00:00 2001 From: eeintech <eeintech@eeinte.ch> Date: Thu, 1 Oct 2020 11:05:08 -0500 Subject: [PATCH 06/77] Fixed 'Part' column sorting --- InvenTree/templates/js/part.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index 59406e7672..bbdca8f8c4 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -168,7 +168,8 @@ function loadParametricPartTable(table, options={}) { * * Args: * - table: HTML reference to the table - * - data: Parameters data + * - table_headers: Table headers/columns + * - table_data: Parameters data */ var table_headers = options.headers @@ -181,9 +182,10 @@ function loadParametricPartTable(table, options={}) { for (header of table_headers) { if (header === 'part') { columns.push({ - field: 'part', + field: header, title: '{% trans 'Part' %}', sortable: true, + sortName: 'name', formatter: function(value, row, index, field) { var name = ''; From 9d3d9a190b390e4d4938b7d457366f9bea9bf035 Mon Sep 17 00:00:00 2001 From: eeintech <eeintech@eeinte.ch> Date: Thu, 1 Oct 2020 12:10:35 -0500 Subject: [PATCH 07/77] Added bootstrap table 'filter-control' extension to use in parametric tables --- .../css/bootstrap-table-filter-control.css | 13 + .../bootstrap-table-filter-control.js | 3021 +++++++++++++++++ .../script/bootstrap/filter-control-utils.js | 2361 +++++++++++++ InvenTree/templates/base.html | 3 + InvenTree/templates/js/part.html | 3 + 5 files changed, 5401 insertions(+) create mode 100644 InvenTree/InvenTree/static/css/bootstrap-table-filter-control.css create mode 100644 InvenTree/InvenTree/static/script/bootstrap/bootstrap-table-filter-control.js create mode 100644 InvenTree/InvenTree/static/script/bootstrap/filter-control-utils.js diff --git a/InvenTree/InvenTree/static/css/bootstrap-table-filter-control.css b/InvenTree/InvenTree/static/css/bootstrap-table-filter-control.css new file mode 100644 index 0000000000..012d6bd897 --- /dev/null +++ b/InvenTree/InvenTree/static/css/bootstrap-table-filter-control.css @@ -0,0 +1,13 @@ +@charset "UTF-8"; +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @version: v2.1.1 + */ +.no-filter-control { + height: 34px; +} + +.filter-control { + margin: 0 2px 2px 2px; +} diff --git a/InvenTree/InvenTree/static/script/bootstrap/bootstrap-table-filter-control.js b/InvenTree/InvenTree/static/script/bootstrap/bootstrap-table-filter-control.js new file mode 100644 index 0000000000..f683c92b03 --- /dev/null +++ b/InvenTree/InvenTree/static/script/bootstrap/bootstrap-table-filter-control.js @@ -0,0 +1,3021 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery')) : + typeof define === 'function' && define.amd ? define(['jquery'], factory) : + (global = global || self, factory(global.jQuery)); +}(this, (function ($) { 'use strict'; + + $ = $ && Object.prototype.hasOwnProperty.call($, 'default') ? $['default'] : $; + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var check = function (it) { + return it && it.Math == Math && it; + }; + + // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 + var global_1 = + // eslint-disable-next-line no-undef + check(typeof globalThis == 'object' && globalThis) || + check(typeof window == 'object' && window) || + check(typeof self == 'object' && self) || + check(typeof commonjsGlobal == 'object' && commonjsGlobal) || + // eslint-disable-next-line no-new-func + Function('return this')(); + + var fails = function (exec) { + try { + return !!exec(); + } catch (error) { + return true; + } + }; + + // Thank's IE8 for his funny defineProperty + var descriptors = !fails(function () { + return Object.defineProperty({}, 'a', { get: function () { return 7; } }).a != 7; + }); + + var nativePropertyIsEnumerable = {}.propertyIsEnumerable; + var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + + // Nashorn ~ JDK8 bug + var NASHORN_BUG = getOwnPropertyDescriptor && !nativePropertyIsEnumerable.call({ 1: 2 }, 1); + + // `Object.prototype.propertyIsEnumerable` method implementation + // https://tc39.github.io/ecma262/#sec-object.prototype.propertyisenumerable + var f = NASHORN_BUG ? function propertyIsEnumerable(V) { + var descriptor = getOwnPropertyDescriptor(this, V); + return !!descriptor && descriptor.enumerable; + } : nativePropertyIsEnumerable; + + var objectPropertyIsEnumerable = { + f: f + }; + + var createPropertyDescriptor = function (bitmap, value) { + return { + enumerable: !(bitmap & 1), + configurable: !(bitmap & 2), + writable: !(bitmap & 4), + value: value + }; + }; + + var toString = {}.toString; + + var classofRaw = function (it) { + return toString.call(it).slice(8, -1); + }; + + var split = ''.split; + + // fallback for non-array-like ES3 and non-enumerable old V8 strings + var indexedObject = fails(function () { + // throws an error in rhino, see https://github.com/mozilla/rhino/issues/346 + // eslint-disable-next-line no-prototype-builtins + return !Object('z').propertyIsEnumerable(0); + }) ? function (it) { + return classofRaw(it) == 'String' ? split.call(it, '') : Object(it); + } : Object; + + // `RequireObjectCoercible` abstract operation + // https://tc39.github.io/ecma262/#sec-requireobjectcoercible + var requireObjectCoercible = function (it) { + if (it == undefined) throw TypeError("Can't call method on " + it); + return it; + }; + + // toObject with fallback for non-array-like ES3 strings + + + + var toIndexedObject = function (it) { + return indexedObject(requireObjectCoercible(it)); + }; + + var isObject = function (it) { + return typeof it === 'object' ? it !== null : typeof it === 'function'; + }; + + // `ToPrimitive` abstract operation + // https://tc39.github.io/ecma262/#sec-toprimitive + // instead of the ES6 spec version, we didn't implement @@toPrimitive case + // and the second argument - flag - preferred type is a string + var toPrimitive = function (input, PREFERRED_STRING) { + if (!isObject(input)) return input; + var fn, val; + if (PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val; + if (typeof (fn = input.valueOf) == 'function' && !isObject(val = fn.call(input))) return val; + if (!PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val; + throw TypeError("Can't convert object to primitive value"); + }; + + var hasOwnProperty = {}.hasOwnProperty; + + var has = function (it, key) { + return hasOwnProperty.call(it, key); + }; + + var document$1 = global_1.document; + // typeof document.createElement is 'object' in old IE + var EXISTS = isObject(document$1) && isObject(document$1.createElement); + + var documentCreateElement = function (it) { + return EXISTS ? document$1.createElement(it) : {}; + }; + + // Thank's IE8 for his funny defineProperty + var ie8DomDefine = !descriptors && !fails(function () { + return Object.defineProperty(documentCreateElement('div'), 'a', { + get: function () { return 7; } + }).a != 7; + }); + + var nativeGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + + // `Object.getOwnPropertyDescriptor` method + // https://tc39.github.io/ecma262/#sec-object.getownpropertydescriptor + var f$1 = descriptors ? nativeGetOwnPropertyDescriptor : function getOwnPropertyDescriptor(O, P) { + O = toIndexedObject(O); + P = toPrimitive(P, true); + if (ie8DomDefine) try { + return nativeGetOwnPropertyDescriptor(O, P); + } catch (error) { /* empty */ } + if (has(O, P)) return createPropertyDescriptor(!objectPropertyIsEnumerable.f.call(O, P), O[P]); + }; + + var objectGetOwnPropertyDescriptor = { + f: f$1 + }; + + var anObject = function (it) { + if (!isObject(it)) { + throw TypeError(String(it) + ' is not an object'); + } return it; + }; + + var nativeDefineProperty = Object.defineProperty; + + // `Object.defineProperty` method + // https://tc39.github.io/ecma262/#sec-object.defineproperty + var f$2 = descriptors ? nativeDefineProperty : function defineProperty(O, P, Attributes) { + anObject(O); + P = toPrimitive(P, true); + anObject(Attributes); + if (ie8DomDefine) try { + return nativeDefineProperty(O, P, Attributes); + } catch (error) { /* empty */ } + if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported'); + if ('value' in Attributes) O[P] = Attributes.value; + return O; + }; + + var objectDefineProperty = { + f: f$2 + }; + + var createNonEnumerableProperty = descriptors ? function (object, key, value) { + return objectDefineProperty.f(object, key, createPropertyDescriptor(1, value)); + } : function (object, key, value) { + object[key] = value; + return object; + }; + + var setGlobal = function (key, value) { + try { + createNonEnumerableProperty(global_1, key, value); + } catch (error) { + global_1[key] = value; + } return value; + }; + + var SHARED = '__core-js_shared__'; + var store = global_1[SHARED] || setGlobal(SHARED, {}); + + var sharedStore = store; + + var functionToString = Function.toString; + + // this helper broken in `3.4.1-3.4.4`, so we can't use `shared` helper + if (typeof sharedStore.inspectSource != 'function') { + sharedStore.inspectSource = function (it) { + return functionToString.call(it); + }; + } + + var inspectSource = sharedStore.inspectSource; + + var WeakMap = global_1.WeakMap; + + var nativeWeakMap = typeof WeakMap === 'function' && /native code/.test(inspectSource(WeakMap)); + + var shared = createCommonjsModule(function (module) { + (module.exports = function (key, value) { + return sharedStore[key] || (sharedStore[key] = value !== undefined ? value : {}); + })('versions', []).push({ + version: '3.6.0', + mode: 'global', + copyright: '© 2019 Denis Pushkarev (zloirock.ru)' + }); + }); + + var id = 0; + var postfix = Math.random(); + + var uid = function (key) { + return 'Symbol(' + String(key === undefined ? '' : key) + ')_' + (++id + postfix).toString(36); + }; + + var keys = shared('keys'); + + var sharedKey = function (key) { + return keys[key] || (keys[key] = uid(key)); + }; + + var hiddenKeys = {}; + + var WeakMap$1 = global_1.WeakMap; + var set, get, has$1; + + var enforce = function (it) { + return has$1(it) ? get(it) : set(it, {}); + }; + + var getterFor = function (TYPE) { + return function (it) { + var state; + if (!isObject(it) || (state = get(it)).type !== TYPE) { + throw TypeError('Incompatible receiver, ' + TYPE + ' required'); + } return state; + }; + }; + + if (nativeWeakMap) { + var store$1 = new WeakMap$1(); + var wmget = store$1.get; + var wmhas = store$1.has; + var wmset = store$1.set; + set = function (it, metadata) { + wmset.call(store$1, it, metadata); + return metadata; + }; + get = function (it) { + return wmget.call(store$1, it) || {}; + }; + has$1 = function (it) { + return wmhas.call(store$1, it); + }; + } else { + var STATE = sharedKey('state'); + hiddenKeys[STATE] = true; + set = function (it, metadata) { + createNonEnumerableProperty(it, STATE, metadata); + return metadata; + }; + get = function (it) { + return has(it, STATE) ? it[STATE] : {}; + }; + has$1 = function (it) { + return has(it, STATE); + }; + } + + var internalState = { + set: set, + get: get, + has: has$1, + enforce: enforce, + getterFor: getterFor + }; + + var redefine = createCommonjsModule(function (module) { + var getInternalState = internalState.get; + var enforceInternalState = internalState.enforce; + var TEMPLATE = String(String).split('String'); + + (module.exports = function (O, key, value, options) { + var unsafe = options ? !!options.unsafe : false; + var simple = options ? !!options.enumerable : false; + var noTargetGet = options ? !!options.noTargetGet : false; + if (typeof value == 'function') { + if (typeof key == 'string' && !has(value, 'name')) createNonEnumerableProperty(value, 'name', key); + enforceInternalState(value).source = TEMPLATE.join(typeof key == 'string' ? key : ''); + } + if (O === global_1) { + if (simple) O[key] = value; + else setGlobal(key, value); + return; + } else if (!unsafe) { + delete O[key]; + } else if (!noTargetGet && O[key]) { + simple = true; + } + if (simple) O[key] = value; + else createNonEnumerableProperty(O, key, value); + // add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative + })(Function.prototype, 'toString', function toString() { + return typeof this == 'function' && getInternalState(this).source || inspectSource(this); + }); + }); + + var path = global_1; + + var aFunction = function (variable) { + return typeof variable == 'function' ? variable : undefined; + }; + + var getBuiltIn = function (namespace, method) { + return arguments.length < 2 ? aFunction(path[namespace]) || aFunction(global_1[namespace]) + : path[namespace] && path[namespace][method] || global_1[namespace] && global_1[namespace][method]; + }; + + var ceil = Math.ceil; + var floor = Math.floor; + + // `ToInteger` abstract operation + // https://tc39.github.io/ecma262/#sec-tointeger + var toInteger = function (argument) { + return isNaN(argument = +argument) ? 0 : (argument > 0 ? floor : ceil)(argument); + }; + + var min = Math.min; + + // `ToLength` abstract operation + // https://tc39.github.io/ecma262/#sec-tolength + var toLength = function (argument) { + return argument > 0 ? min(toInteger(argument), 0x1FFFFFFFFFFFFF) : 0; // 2 ** 53 - 1 == 9007199254740991 + }; + + var max = Math.max; + var min$1 = Math.min; + + // Helper for a popular repeating case of the spec: + // Let integer be ? ToInteger(index). + // If integer < 0, let result be max((length + integer), 0); else let result be min(integer, length). + var toAbsoluteIndex = function (index, length) { + var integer = toInteger(index); + return integer < 0 ? max(integer + length, 0) : min$1(integer, length); + }; + + // `Array.prototype.{ indexOf, includes }` methods implementation + var createMethod = function (IS_INCLUDES) { + return function ($this, el, fromIndex) { + var O = toIndexedObject($this); + var length = toLength(O.length); + var index = toAbsoluteIndex(fromIndex, length); + var value; + // Array#includes uses SameValueZero equality algorithm + // eslint-disable-next-line no-self-compare + if (IS_INCLUDES && el != el) while (length > index) { + value = O[index++]; + // eslint-disable-next-line no-self-compare + if (value != value) return true; + // Array#indexOf ignores holes, Array#includes - not + } else for (;length > index; index++) { + if ((IS_INCLUDES || index in O) && O[index] === el) return IS_INCLUDES || index || 0; + } return !IS_INCLUDES && -1; + }; + }; + + var arrayIncludes = { + // `Array.prototype.includes` method + // https://tc39.github.io/ecma262/#sec-array.prototype.includes + includes: createMethod(true), + // `Array.prototype.indexOf` method + // https://tc39.github.io/ecma262/#sec-array.prototype.indexof + indexOf: createMethod(false) + }; + + var indexOf = arrayIncludes.indexOf; + + + var objectKeysInternal = function (object, names) { + var O = toIndexedObject(object); + var i = 0; + var result = []; + var key; + for (key in O) !has(hiddenKeys, key) && has(O, key) && result.push(key); + // Don't enum bug & hidden keys + while (names.length > i) if (has(O, key = names[i++])) { + ~indexOf(result, key) || result.push(key); + } + return result; + }; + + // IE8- don't enum bug keys + var enumBugKeys = [ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toLocaleString', + 'toString', + 'valueOf' + ]; + + var hiddenKeys$1 = enumBugKeys.concat('length', 'prototype'); + + // `Object.getOwnPropertyNames` method + // https://tc39.github.io/ecma262/#sec-object.getownpropertynames + var f$3 = Object.getOwnPropertyNames || function getOwnPropertyNames(O) { + return objectKeysInternal(O, hiddenKeys$1); + }; + + var objectGetOwnPropertyNames = { + f: f$3 + }; + + var f$4 = Object.getOwnPropertySymbols; + + var objectGetOwnPropertySymbols = { + f: f$4 + }; + + // all object keys, includes non-enumerable and symbols + var ownKeys = getBuiltIn('Reflect', 'ownKeys') || function ownKeys(it) { + var keys = objectGetOwnPropertyNames.f(anObject(it)); + var getOwnPropertySymbols = objectGetOwnPropertySymbols.f; + return getOwnPropertySymbols ? keys.concat(getOwnPropertySymbols(it)) : keys; + }; + + var copyConstructorProperties = function (target, source) { + var keys = ownKeys(source); + var defineProperty = objectDefineProperty.f; + var getOwnPropertyDescriptor = objectGetOwnPropertyDescriptor.f; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (!has(target, key)) defineProperty(target, key, getOwnPropertyDescriptor(source, key)); + } + }; + + var replacement = /#|\.prototype\./; + + var isForced = function (feature, detection) { + var value = data[normalize(feature)]; + return value == POLYFILL ? true + : value == NATIVE ? false + : typeof detection == 'function' ? fails(detection) + : !!detection; + }; + + var normalize = isForced.normalize = function (string) { + return String(string).replace(replacement, '.').toLowerCase(); + }; + + var data = isForced.data = {}; + var NATIVE = isForced.NATIVE = 'N'; + var POLYFILL = isForced.POLYFILL = 'P'; + + var isForced_1 = isForced; + + var getOwnPropertyDescriptor$1 = objectGetOwnPropertyDescriptor.f; + + + + + + + /* + options.target - name of the target object + options.global - target is the global object + options.stat - export as static methods of target + options.proto - export as prototype methods of target + options.real - real prototype method for the `pure` version + options.forced - export even if the native feature is available + options.bind - bind methods to the target, required for the `pure` version + options.wrap - wrap constructors to preventing global pollution, required for the `pure` version + options.unsafe - use the simple assignment of property instead of delete + defineProperty + options.sham - add a flag to not completely full polyfills + options.enumerable - export as enumerable property + options.noTargetGet - prevent calling a getter on target + */ + var _export = function (options, source) { + var TARGET = options.target; + var GLOBAL = options.global; + var STATIC = options.stat; + var FORCED, target, key, targetProperty, sourceProperty, descriptor; + if (GLOBAL) { + target = global_1; + } else if (STATIC) { + target = global_1[TARGET] || setGlobal(TARGET, {}); + } else { + target = (global_1[TARGET] || {}).prototype; + } + if (target) for (key in source) { + sourceProperty = source[key]; + if (options.noTargetGet) { + descriptor = getOwnPropertyDescriptor$1(target, key); + targetProperty = descriptor && descriptor.value; + } else targetProperty = target[key]; + FORCED = isForced_1(GLOBAL ? key : TARGET + (STATIC ? '.' : '#') + key, options.forced); + // contained in target + if (!FORCED && targetProperty !== undefined) { + if (typeof sourceProperty === typeof targetProperty) continue; + copyConstructorProperties(sourceProperty, targetProperty); + } + // add a flag to not completely full polyfills + if (options.sham || (targetProperty && targetProperty.sham)) { + createNonEnumerableProperty(sourceProperty, 'sham', true); + } + // extend global + redefine(target, key, sourceProperty, options); + } + }; + + // `IsArray` abstract operation + // https://tc39.github.io/ecma262/#sec-isarray + var isArray = Array.isArray || function isArray(arg) { + return classofRaw(arg) == 'Array'; + }; + + // `ToObject` abstract operation + // https://tc39.github.io/ecma262/#sec-toobject + var toObject = function (argument) { + return Object(requireObjectCoercible(argument)); + }; + + var createProperty = function (object, key, value) { + var propertyKey = toPrimitive(key); + if (propertyKey in object) objectDefineProperty.f(object, propertyKey, createPropertyDescriptor(0, value)); + else object[propertyKey] = value; + }; + + var nativeSymbol = !!Object.getOwnPropertySymbols && !fails(function () { + // Chrome 38 Symbol has incorrect toString conversion + // eslint-disable-next-line no-undef + return !String(Symbol()); + }); + + var useSymbolAsUid = nativeSymbol + // eslint-disable-next-line no-undef + && !Symbol.sham + // eslint-disable-next-line no-undef + && typeof Symbol() == 'symbol'; + + var WellKnownSymbolsStore = shared('wks'); + var Symbol$1 = global_1.Symbol; + var createWellKnownSymbol = useSymbolAsUid ? Symbol$1 : uid; + + var wellKnownSymbol = function (name) { + if (!has(WellKnownSymbolsStore, name)) { + if (nativeSymbol && has(Symbol$1, name)) WellKnownSymbolsStore[name] = Symbol$1[name]; + else WellKnownSymbolsStore[name] = createWellKnownSymbol('Symbol.' + name); + } return WellKnownSymbolsStore[name]; + }; + + var SPECIES = wellKnownSymbol('species'); + + // `ArraySpeciesCreate` abstract operation + // https://tc39.github.io/ecma262/#sec-arrayspeciescreate + var arraySpeciesCreate = function (originalArray, length) { + var C; + if (isArray(originalArray)) { + C = originalArray.constructor; + // cross-realm fallback + if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined; + else if (isObject(C)) { + C = C[SPECIES]; + if (C === null) C = undefined; + } + } return new (C === undefined ? Array : C)(length === 0 ? 0 : length); + }; + + var userAgent = getBuiltIn('navigator', 'userAgent') || ''; + + var process = global_1.process; + var versions = process && process.versions; + var v8 = versions && versions.v8; + var match, version; + + if (v8) { + match = v8.split('.'); + version = match[0] + match[1]; + } else if (userAgent) { + match = userAgent.match(/Edge\/(\d+)/); + if (!match || match[1] >= 74) { + match = userAgent.match(/Chrome\/(\d+)/); + if (match) version = match[1]; + } + } + + var v8Version = version && +version; + + var SPECIES$1 = wellKnownSymbol('species'); + + var arrayMethodHasSpeciesSupport = function (METHOD_NAME) { + // We can't use this feature detection in V8 since it causes + // deoptimization and serious performance degradation + // https://github.com/zloirock/core-js/issues/677 + return v8Version >= 51 || !fails(function () { + var array = []; + var constructor = array.constructor = {}; + constructor[SPECIES$1] = function () { + return { foo: 1 }; + }; + return array[METHOD_NAME](Boolean).foo !== 1; + }); + }; + + var IS_CONCAT_SPREADABLE = wellKnownSymbol('isConcatSpreadable'); + var MAX_SAFE_INTEGER = 0x1FFFFFFFFFFFFF; + var MAXIMUM_ALLOWED_INDEX_EXCEEDED = 'Maximum allowed index exceeded'; + + // We can't use this feature detection in V8 since it causes + // deoptimization and serious performance degradation + // https://github.com/zloirock/core-js/issues/679 + var IS_CONCAT_SPREADABLE_SUPPORT = v8Version >= 51 || !fails(function () { + var array = []; + array[IS_CONCAT_SPREADABLE] = false; + return array.concat()[0] !== array; + }); + + var SPECIES_SUPPORT = arrayMethodHasSpeciesSupport('concat'); + + var isConcatSpreadable = function (O) { + if (!isObject(O)) return false; + var spreadable = O[IS_CONCAT_SPREADABLE]; + return spreadable !== undefined ? !!spreadable : isArray(O); + }; + + var FORCED = !IS_CONCAT_SPREADABLE_SUPPORT || !SPECIES_SUPPORT; + + // `Array.prototype.concat` method + // https://tc39.github.io/ecma262/#sec-array.prototype.concat + // with adding support of @@isConcatSpreadable and @@species + _export({ target: 'Array', proto: true, forced: FORCED }, { + concat: function concat(arg) { // eslint-disable-line no-unused-vars + var O = toObject(this); + var A = arraySpeciesCreate(O, 0); + var n = 0; + var i, k, length, len, E; + for (i = -1, length = arguments.length; i < length; i++) { + E = i === -1 ? O : arguments[i]; + if (isConcatSpreadable(E)) { + len = toLength(E.length); + if (n + len > MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED); + for (k = 0; k < len; k++, n++) if (k in E) createProperty(A, n, E[k]); + } else { + if (n >= MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED); + createProperty(A, n++, E); + } + } + A.length = n; + return A; + } + }); + + var aFunction$1 = function (it) { + if (typeof it != 'function') { + throw TypeError(String(it) + ' is not a function'); + } return it; + }; + + // optional / simple context binding + var bindContext = function (fn, that, length) { + aFunction$1(fn); + if (that === undefined) return fn; + switch (length) { + case 0: return function () { + return fn.call(that); + }; + case 1: return function (a) { + return fn.call(that, a); + }; + case 2: return function (a, b) { + return fn.call(that, a, b); + }; + case 3: return function (a, b, c) { + return fn.call(that, a, b, c); + }; + } + return function (/* ...args */) { + return fn.apply(that, arguments); + }; + }; + + var push = [].push; + + // `Array.prototype.{ forEach, map, filter, some, every, find, findIndex }` methods implementation + var createMethod$1 = function (TYPE) { + var IS_MAP = TYPE == 1; + var IS_FILTER = TYPE == 2; + var IS_SOME = TYPE == 3; + var IS_EVERY = TYPE == 4; + var IS_FIND_INDEX = TYPE == 6; + var NO_HOLES = TYPE == 5 || IS_FIND_INDEX; + return function ($this, callbackfn, that, specificCreate) { + var O = toObject($this); + var self = indexedObject(O); + var boundFunction = bindContext(callbackfn, that, 3); + var length = toLength(self.length); + var index = 0; + var create = specificCreate || arraySpeciesCreate; + var target = IS_MAP ? create($this, length) : IS_FILTER ? create($this, 0) : undefined; + var value, result; + for (;length > index; index++) if (NO_HOLES || index in self) { + value = self[index]; + result = boundFunction(value, index, O); + if (TYPE) { + if (IS_MAP) target[index] = result; // map + else if (result) switch (TYPE) { + case 3: return true; // some + case 5: return value; // find + case 6: return index; // findIndex + case 2: push.call(target, value); // filter + } else if (IS_EVERY) return false; // every + } + } + return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : target; + }; + }; + + var arrayIteration = { + // `Array.prototype.forEach` method + // https://tc39.github.io/ecma262/#sec-array.prototype.foreach + forEach: createMethod$1(0), + // `Array.prototype.map` method + // https://tc39.github.io/ecma262/#sec-array.prototype.map + map: createMethod$1(1), + // `Array.prototype.filter` method + // https://tc39.github.io/ecma262/#sec-array.prototype.filter + filter: createMethod$1(2), + // `Array.prototype.some` method + // https://tc39.github.io/ecma262/#sec-array.prototype.some + some: createMethod$1(3), + // `Array.prototype.every` method + // https://tc39.github.io/ecma262/#sec-array.prototype.every + every: createMethod$1(4), + // `Array.prototype.find` method + // https://tc39.github.io/ecma262/#sec-array.prototype.find + find: createMethod$1(5), + // `Array.prototype.findIndex` method + // https://tc39.github.io/ecma262/#sec-array.prototype.findIndex + findIndex: createMethod$1(6) + }; + + var $filter = arrayIteration.filter; + + + + var HAS_SPECIES_SUPPORT = arrayMethodHasSpeciesSupport('filter'); + // Edge 14- issue + var USES_TO_LENGTH = HAS_SPECIES_SUPPORT && !fails(function () { + [].filter.call({ length: -1, 0: 1 }, function (it) { throw it; }); + }); + + // `Array.prototype.filter` method + // https://tc39.github.io/ecma262/#sec-array.prototype.filter + // with adding support of @@species + _export({ target: 'Array', proto: true, forced: !HAS_SPECIES_SUPPORT || !USES_TO_LENGTH }, { + filter: function filter(callbackfn /* , thisArg */) { + return $filter(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + // `Object.keys` method + // https://tc39.github.io/ecma262/#sec-object.keys + var objectKeys = Object.keys || function keys(O) { + return objectKeysInternal(O, enumBugKeys); + }; + + // `Object.defineProperties` method + // https://tc39.github.io/ecma262/#sec-object.defineproperties + var objectDefineProperties = descriptors ? Object.defineProperties : function defineProperties(O, Properties) { + anObject(O); + var keys = objectKeys(Properties); + var length = keys.length; + var index = 0; + var key; + while (length > index) objectDefineProperty.f(O, key = keys[index++], Properties[key]); + return O; + }; + + var html = getBuiltIn('document', 'documentElement'); + + var GT = '>'; + var LT = '<'; + var PROTOTYPE = 'prototype'; + var SCRIPT = 'script'; + var IE_PROTO = sharedKey('IE_PROTO'); + + var EmptyConstructor = function () { /* empty */ }; + + var scriptTag = function (content) { + return LT + SCRIPT + GT + content + LT + '/' + SCRIPT + GT; + }; + + // Create object with fake `null` prototype: use ActiveX Object with cleared prototype + var NullProtoObjectViaActiveX = function (activeXDocument) { + activeXDocument.write(scriptTag('')); + activeXDocument.close(); + var temp = activeXDocument.parentWindow.Object; + activeXDocument = null; // avoid memory leak + return temp; + }; + + // Create object with fake `null` prototype: use iframe Object with cleared prototype + var NullProtoObjectViaIFrame = function () { + // Thrash, waste and sodomy: IE GC bug + var iframe = documentCreateElement('iframe'); + var JS = 'java' + SCRIPT + ':'; + var iframeDocument; + iframe.style.display = 'none'; + html.appendChild(iframe); + // https://github.com/zloirock/core-js/issues/475 + iframe.src = String(JS); + iframeDocument = iframe.contentWindow.document; + iframeDocument.open(); + iframeDocument.write(scriptTag('document.F=Object')); + iframeDocument.close(); + return iframeDocument.F; + }; + + // Check for document.domain and active x support + // No need to use active x approach when document.domain is not set + // see https://github.com/es-shims/es5-shim/issues/150 + // variation of https://github.com/kitcambridge/es5-shim/commit/4f738ac066346 + // avoid IE GC bug + var activeXDocument; + var NullProtoObject = function () { + try { + /* global ActiveXObject */ + activeXDocument = document.domain && new ActiveXObject('htmlfile'); + } catch (error) { /* ignore */ } + NullProtoObject = activeXDocument ? NullProtoObjectViaActiveX(activeXDocument) : NullProtoObjectViaIFrame(); + var length = enumBugKeys.length; + while (length--) delete NullProtoObject[PROTOTYPE][enumBugKeys[length]]; + return NullProtoObject(); + }; + + hiddenKeys[IE_PROTO] = true; + + // `Object.create` method + // https://tc39.github.io/ecma262/#sec-object.create + var objectCreate = Object.create || function create(O, Properties) { + var result; + if (O !== null) { + EmptyConstructor[PROTOTYPE] = anObject(O); + result = new EmptyConstructor(); + EmptyConstructor[PROTOTYPE] = null; + // add "__proto__" for Object.getPrototypeOf polyfill + result[IE_PROTO] = O; + } else result = NullProtoObject(); + return Properties === undefined ? result : objectDefineProperties(result, Properties); + }; + + var UNSCOPABLES = wellKnownSymbol('unscopables'); + var ArrayPrototype = Array.prototype; + + // Array.prototype[@@unscopables] + // https://tc39.github.io/ecma262/#sec-array.prototype-@@unscopables + if (ArrayPrototype[UNSCOPABLES] == undefined) { + objectDefineProperty.f(ArrayPrototype, UNSCOPABLES, { + configurable: true, + value: objectCreate(null) + }); + } + + // add a key to Array.prototype[@@unscopables] + var addToUnscopables = function (key) { + ArrayPrototype[UNSCOPABLES][key] = true; + }; + + var $find = arrayIteration.find; + + + var FIND = 'find'; + var SKIPS_HOLES = true; + + // Shouldn't skip holes + if (FIND in []) Array(1)[FIND](function () { SKIPS_HOLES = false; }); + + // `Array.prototype.find` method + // https://tc39.github.io/ecma262/#sec-array.prototype.find + _export({ target: 'Array', proto: true, forced: SKIPS_HOLES }, { + find: function find(callbackfn /* , that = undefined */) { + return $find(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + // https://tc39.github.io/ecma262/#sec-array.prototype-@@unscopables + addToUnscopables(FIND); + + var $includes = arrayIncludes.includes; + + + // `Array.prototype.includes` method + // https://tc39.github.io/ecma262/#sec-array.prototype.includes + _export({ target: 'Array', proto: true }, { + includes: function includes(el /* , fromIndex = 0 */) { + return $includes(this, el, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + // https://tc39.github.io/ecma262/#sec-array.prototype-@@unscopables + addToUnscopables('includes'); + + var sloppyArrayMethod = function (METHOD_NAME, argument) { + var method = [][METHOD_NAME]; + return !method || !fails(function () { + // eslint-disable-next-line no-useless-call,no-throw-literal + method.call(null, argument || function () { throw 1; }, 1); + }); + }; + + var $indexOf = arrayIncludes.indexOf; + + + var nativeIndexOf = [].indexOf; + + var NEGATIVE_ZERO = !!nativeIndexOf && 1 / [1].indexOf(1, -0) < 0; + var SLOPPY_METHOD = sloppyArrayMethod('indexOf'); + + // `Array.prototype.indexOf` method + // https://tc39.github.io/ecma262/#sec-array.prototype.indexof + _export({ target: 'Array', proto: true, forced: NEGATIVE_ZERO || SLOPPY_METHOD }, { + indexOf: function indexOf(searchElement /* , fromIndex = 0 */) { + return NEGATIVE_ZERO + // convert -0 to +0 + ? nativeIndexOf.apply(this, arguments) || 0 + : $indexOf(this, searchElement, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + var nativeAssign = Object.assign; + var defineProperty = Object.defineProperty; + + // `Object.assign` method + // https://tc39.github.io/ecma262/#sec-object.assign + var objectAssign = !nativeAssign || fails(function () { + // should have correct order of operations (Edge bug) + if (descriptors && nativeAssign({ b: 1 }, nativeAssign(defineProperty({}, 'a', { + enumerable: true, + get: function () { + defineProperty(this, 'b', { + value: 3, + enumerable: false + }); + } + }), { b: 2 })).b !== 1) return true; + // should work with symbols and should have deterministic property order (V8 bug) + var A = {}; + var B = {}; + // eslint-disable-next-line no-undef + var symbol = Symbol(); + var alphabet = 'abcdefghijklmnopqrst'; + A[symbol] = 7; + alphabet.split('').forEach(function (chr) { B[chr] = chr; }); + return nativeAssign({}, A)[symbol] != 7 || objectKeys(nativeAssign({}, B)).join('') != alphabet; + }) ? function assign(target, source) { // eslint-disable-line no-unused-vars + var T = toObject(target); + var argumentsLength = arguments.length; + var index = 1; + var getOwnPropertySymbols = objectGetOwnPropertySymbols.f; + var propertyIsEnumerable = objectPropertyIsEnumerable.f; + while (argumentsLength > index) { + var S = indexedObject(arguments[index++]); + var keys = getOwnPropertySymbols ? objectKeys(S).concat(getOwnPropertySymbols(S)) : objectKeys(S); + var length = keys.length; + var j = 0; + var key; + while (length > j) { + key = keys[j++]; + if (!descriptors || propertyIsEnumerable.call(S, key)) T[key] = S[key]; + } + } return T; + } : nativeAssign; + + // `Object.assign` method + // https://tc39.github.io/ecma262/#sec-object.assign + _export({ target: 'Object', stat: true, forced: Object.assign !== objectAssign }, { + assign: objectAssign + }); + + var FAILS_ON_PRIMITIVES = fails(function () { objectKeys(1); }); + + // `Object.keys` method + // https://tc39.github.io/ecma262/#sec-object.keys + _export({ target: 'Object', stat: true, forced: FAILS_ON_PRIMITIVES }, { + keys: function keys(it) { + return objectKeys(toObject(it)); + } + }); + + var TO_STRING_TAG = wellKnownSymbol('toStringTag'); + var test = {}; + + test[TO_STRING_TAG] = 'z'; + + var toStringTagSupport = String(test) === '[object z]'; + + var TO_STRING_TAG$1 = wellKnownSymbol('toStringTag'); + // ES3 wrong here + var CORRECT_ARGUMENTS = classofRaw(function () { return arguments; }()) == 'Arguments'; + + // fallback for IE11 Script Access Denied error + var tryGet = function (it, key) { + try { + return it[key]; + } catch (error) { /* empty */ } + }; + + // getting tag from ES6+ `Object.prototype.toString` + var classof = toStringTagSupport ? classofRaw : function (it) { + var O, tag, result; + return it === undefined ? 'Undefined' : it === null ? 'Null' + // @@toStringTag case + : typeof (tag = tryGet(O = Object(it), TO_STRING_TAG$1)) == 'string' ? tag + // builtinTag case + : CORRECT_ARGUMENTS ? classofRaw(O) + // ES3 arguments fallback + : (result = classofRaw(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : result; + }; + + // `Object.prototype.toString` method implementation + // https://tc39.github.io/ecma262/#sec-object.prototype.tostring + var objectToString = toStringTagSupport ? {}.toString : function toString() { + return '[object ' + classof(this) + ']'; + }; + + // `Object.prototype.toString` method + // https://tc39.github.io/ecma262/#sec-object.prototype.tostring + if (!toStringTagSupport) { + redefine(Object.prototype, 'toString', objectToString, { unsafe: true }); + } + + // a string of all valid unicode whitespaces + // eslint-disable-next-line max-len + var whitespaces = '\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF'; + + var whitespace = '[' + whitespaces + ']'; + var ltrim = RegExp('^' + whitespace + whitespace + '*'); + var rtrim = RegExp(whitespace + whitespace + '*$'); + + // `String.prototype.{ trim, trimStart, trimEnd, trimLeft, trimRight }` methods implementation + var createMethod$2 = function (TYPE) { + return function ($this) { + var string = String(requireObjectCoercible($this)); + if (TYPE & 1) string = string.replace(ltrim, ''); + if (TYPE & 2) string = string.replace(rtrim, ''); + return string; + }; + }; + + var stringTrim = { + // `String.prototype.{ trimLeft, trimStart }` methods + // https://tc39.github.io/ecma262/#sec-string.prototype.trimstart + start: createMethod$2(1), + // `String.prototype.{ trimRight, trimEnd }` methods + // https://tc39.github.io/ecma262/#sec-string.prototype.trimend + end: createMethod$2(2), + // `String.prototype.trim` method + // https://tc39.github.io/ecma262/#sec-string.prototype.trim + trim: createMethod$2(3) + }; + + var trim = stringTrim.trim; + + + var nativeParseInt = global_1.parseInt; + var hex = /^[+-]?0[Xx]/; + var FORCED$1 = nativeParseInt(whitespaces + '08') !== 8 || nativeParseInt(whitespaces + '0x16') !== 22; + + // `parseInt` method + // https://tc39.github.io/ecma262/#sec-parseint-string-radix + var _parseInt = FORCED$1 ? function parseInt(string, radix) { + var S = trim(String(string)); + return nativeParseInt(S, (radix >>> 0) || (hex.test(S) ? 16 : 10)); + } : nativeParseInt; + + // `parseInt` method + // https://tc39.github.io/ecma262/#sec-parseint-string-radix + _export({ global: true, forced: parseInt != _parseInt }, { + parseInt: _parseInt + }); + + // `RegExp.prototype.flags` getter implementation + // https://tc39.github.io/ecma262/#sec-get-regexp.prototype.flags + var regexpFlags = function () { + var that = anObject(this); + var result = ''; + if (that.global) result += 'g'; + if (that.ignoreCase) result += 'i'; + if (that.multiline) result += 'm'; + if (that.dotAll) result += 's'; + if (that.unicode) result += 'u'; + if (that.sticky) result += 'y'; + return result; + }; + + // babel-minify transpiles RegExp('a', 'y') -> /a/y and it causes SyntaxError, + // so we use an intermediate function. + function RE(s, f) { + return RegExp(s, f); + } + + var UNSUPPORTED_Y = fails(function () { + // babel-minify transpiles RegExp('a', 'y') -> /a/y and it causes SyntaxError + var re = RE('a', 'y'); + re.lastIndex = 2; + return re.exec('abcd') != null; + }); + + var BROKEN_CARET = fails(function () { + // https://bugzilla.mozilla.org/show_bug.cgi?id=773687 + var re = RE('^r', 'gy'); + re.lastIndex = 2; + return re.exec('str') != null; + }); + + var regexpStickyHelpers = { + UNSUPPORTED_Y: UNSUPPORTED_Y, + BROKEN_CARET: BROKEN_CARET + }; + + var nativeExec = RegExp.prototype.exec; + // This always refers to the native implementation, because the + // String#replace polyfill uses ./fix-regexp-well-known-symbol-logic.js, + // which loads this file before patching the method. + var nativeReplace = String.prototype.replace; + + var patchedExec = nativeExec; + + var UPDATES_LAST_INDEX_WRONG = (function () { + var re1 = /a/; + var re2 = /b*/g; + nativeExec.call(re1, 'a'); + nativeExec.call(re2, 'a'); + return re1.lastIndex !== 0 || re2.lastIndex !== 0; + })(); + + var UNSUPPORTED_Y$1 = regexpStickyHelpers.UNSUPPORTED_Y || regexpStickyHelpers.BROKEN_CARET; + + // nonparticipating capturing group, copied from es5-shim's String#split patch. + var NPCG_INCLUDED = /()??/.exec('')[1] !== undefined; + + var PATCH = UPDATES_LAST_INDEX_WRONG || NPCG_INCLUDED || UNSUPPORTED_Y$1; + + if (PATCH) { + patchedExec = function exec(str) { + var re = this; + var lastIndex, reCopy, match, i; + var sticky = UNSUPPORTED_Y$1 && re.sticky; + var flags = regexpFlags.call(re); + var source = re.source; + var charsAdded = 0; + var strCopy = str; + + if (sticky) { + flags = flags.replace('y', ''); + if (flags.indexOf('g') === -1) { + flags += 'g'; + } + + strCopy = String(str).slice(re.lastIndex); + // Support anchored sticky behavior. + if (re.lastIndex > 0 && (!re.multiline || re.multiline && str[re.lastIndex - 1] !== '\n')) { + source = '(?: ' + source + ')'; + strCopy = ' ' + strCopy; + charsAdded++; + } + // ^(? + rx + ) is needed, in combination with some str slicing, to + // simulate the 'y' flag. + reCopy = new RegExp('^(?:' + source + ')', flags); + } + + if (NPCG_INCLUDED) { + reCopy = new RegExp('^' + source + '$(?!\\s)', flags); + } + if (UPDATES_LAST_INDEX_WRONG) lastIndex = re.lastIndex; + + match = nativeExec.call(sticky ? reCopy : re, strCopy); + + if (sticky) { + if (match) { + match.input = match.input.slice(charsAdded); + match[0] = match[0].slice(charsAdded); + match.index = re.lastIndex; + re.lastIndex += match[0].length; + } else re.lastIndex = 0; + } else if (UPDATES_LAST_INDEX_WRONG && match) { + re.lastIndex = re.global ? match.index + match[0].length : lastIndex; + } + if (NPCG_INCLUDED && match && match.length > 1) { + // Fix browsers whose `exec` methods don't consistently return `undefined` + // for NPCG, like IE8. NOTE: This doesn' work for /(.?)?/ + nativeReplace.call(match[0], reCopy, function () { + for (i = 1; i < arguments.length - 2; i++) { + if (arguments[i] === undefined) match[i] = undefined; + } + }); + } + + return match; + }; + } + + var regexpExec = patchedExec; + + _export({ target: 'RegExp', proto: true, forced: /./.exec !== regexpExec }, { + exec: regexpExec + }); + + var TO_STRING = 'toString'; + var RegExpPrototype = RegExp.prototype; + var nativeToString = RegExpPrototype[TO_STRING]; + + var NOT_GENERIC = fails(function () { return nativeToString.call({ source: 'a', flags: 'b' }) != '/a/b'; }); + // FF44- RegExp#toString has a wrong name + var INCORRECT_NAME = nativeToString.name != TO_STRING; + + // `RegExp.prototype.toString` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype.tostring + if (NOT_GENERIC || INCORRECT_NAME) { + redefine(RegExp.prototype, TO_STRING, function toString() { + var R = anObject(this); + var p = String(R.source); + var rf = R.flags; + var f = String(rf === undefined && R instanceof RegExp && !('flags' in RegExpPrototype) ? regexpFlags.call(R) : rf); + return '/' + p + '/' + f; + }, { unsafe: true }); + } + + var MATCH = wellKnownSymbol('match'); + + // `IsRegExp` abstract operation + // https://tc39.github.io/ecma262/#sec-isregexp + var isRegexp = function (it) { + var isRegExp; + return isObject(it) && ((isRegExp = it[MATCH]) !== undefined ? !!isRegExp : classofRaw(it) == 'RegExp'); + }; + + var notARegexp = function (it) { + if (isRegexp(it)) { + throw TypeError("The method doesn't accept regular expressions"); + } return it; + }; + + var MATCH$1 = wellKnownSymbol('match'); + + var correctIsRegexpLogic = function (METHOD_NAME) { + var regexp = /./; + try { + '/./'[METHOD_NAME](regexp); + } catch (e) { + try { + regexp[MATCH$1] = false; + return '/./'[METHOD_NAME](regexp); + } catch (f) { /* empty */ } + } return false; + }; + + // `String.prototype.includes` method + // https://tc39.github.io/ecma262/#sec-string.prototype.includes + _export({ target: 'String', proto: true, forced: !correctIsRegexpLogic('includes') }, { + includes: function includes(searchString /* , position = 0 */) { + return !!~String(requireObjectCoercible(this)) + .indexOf(notARegexp(searchString), arguments.length > 1 ? arguments[1] : undefined); + } + }); + + var non = '\u200B\u0085\u180E'; + + // check that a method works with the correct list + // of whitespaces and has a correct name + var forcedStringTrimMethod = function (METHOD_NAME) { + return fails(function () { + return !!whitespaces[METHOD_NAME]() || non[METHOD_NAME]() != non || whitespaces[METHOD_NAME].name !== METHOD_NAME; + }); + }; + + var $trim = stringTrim.trim; + + + // `String.prototype.trim` method + // https://tc39.github.io/ecma262/#sec-string.prototype.trim + _export({ target: 'String', proto: true, forced: forcedStringTrimMethod('trim') }, { + trim: function trim() { + return $trim(this); + } + }); + + // iterable DOM collections + // flag - `iterable` interface - 'entries', 'keys', 'values', 'forEach' methods + var domIterables = { + CSSRuleList: 0, + CSSStyleDeclaration: 0, + CSSValueList: 0, + ClientRectList: 0, + DOMRectList: 0, + DOMStringList: 0, + DOMTokenList: 1, + DataTransferItemList: 0, + FileList: 0, + HTMLAllCollection: 0, + HTMLCollection: 0, + HTMLFormElement: 0, + HTMLSelectElement: 0, + MediaList: 0, + MimeTypeArray: 0, + NamedNodeMap: 0, + NodeList: 1, + PaintRequestList: 0, + Plugin: 0, + PluginArray: 0, + SVGLengthList: 0, + SVGNumberList: 0, + SVGPathSegList: 0, + SVGPointList: 0, + SVGStringList: 0, + SVGTransformList: 0, + SourceBufferList: 0, + StyleSheetList: 0, + TextTrackCueList: 0, + TextTrackList: 0, + TouchList: 0 + }; + + var $forEach = arrayIteration.forEach; + + + // `Array.prototype.forEach` method implementation + // https://tc39.github.io/ecma262/#sec-array.prototype.foreach + var arrayForEach = sloppyArrayMethod('forEach') ? function forEach(callbackfn /* , thisArg */) { + return $forEach(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined); + } : [].forEach; + + for (var COLLECTION_NAME in domIterables) { + var Collection = global_1[COLLECTION_NAME]; + var CollectionPrototype = Collection && Collection.prototype; + // some Chrome versions have non-configurable methods on DOMTokenList + if (CollectionPrototype && CollectionPrototype.forEach !== arrayForEach) try { + createNonEnumerableProperty(CollectionPrototype, 'forEach', arrayForEach); + } catch (error) { + CollectionPrototype.forEach = arrayForEach; + } + } + + function _typeof(obj) { + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function (obj) { + return typeof obj; + }; + } else { + _typeof = function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + + return _typeof(obj); + } + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function"); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + writable: true, + configurable: true + } + }); + if (superClass) _setPrototypeOf(subClass, superClass); + } + + function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); + } + + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + + return _setPrototypeOf(o, p); + } + + function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return self; + } + + function _possibleConstructorReturn(self, call) { + if (call && (typeof call === "object" || typeof call === "function")) { + return call; + } + + return _assertThisInitialized(self); + } + + function _superPropBase(object, property) { + while (!Object.prototype.hasOwnProperty.call(object, property)) { + object = _getPrototypeOf(object); + if (object === null) break; + } + + return object; + } + + function _get(target, property, receiver) { + if (typeof Reflect !== "undefined" && Reflect.get) { + _get = Reflect.get; + } else { + _get = function _get(target, property, receiver) { + var base = _superPropBase(target, property); + + if (!base) return; + var desc = Object.getOwnPropertyDescriptor(base, property); + + if (desc.get) { + return desc.get.call(receiver); + } + + return desc.value; + }; + } + + return _get(target, property, receiver || target); + } + + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); + } + + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; + + return arr2; + } + } + + function _iterableToArray(iter) { + if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); + } + + function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance"); + } + + var nativeJoin = [].join; + + var ES3_STRINGS = indexedObject != Object; + var SLOPPY_METHOD$1 = sloppyArrayMethod('join', ','); + + // `Array.prototype.join` method + // https://tc39.github.io/ecma262/#sec-array.prototype.join + _export({ target: 'Array', proto: true, forced: ES3_STRINGS || SLOPPY_METHOD$1 }, { + join: function join(separator) { + return nativeJoin.call(toIndexedObject(this), separator === undefined ? ',' : separator); + } + }); + + var test$1 = []; + var nativeSort = test$1.sort; + + // IE8- + var FAILS_ON_UNDEFINED = fails(function () { + test$1.sort(undefined); + }); + // V8 bug + var FAILS_ON_NULL = fails(function () { + test$1.sort(null); + }); + // Old WebKit + var SLOPPY_METHOD$2 = sloppyArrayMethod('sort'); + + var FORCED$2 = FAILS_ON_UNDEFINED || !FAILS_ON_NULL || SLOPPY_METHOD$2; + + // `Array.prototype.sort` method + // https://tc39.github.io/ecma262/#sec-array.prototype.sort + _export({ target: 'Array', proto: true, forced: FORCED$2 }, { + sort: function sort(comparefn) { + return comparefn === undefined + ? nativeSort.call(toObject(this)) + : nativeSort.call(toObject(this), aFunction$1(comparefn)); + } + }); + + var SPECIES$2 = wellKnownSymbol('species'); + + var REPLACE_SUPPORTS_NAMED_GROUPS = !fails(function () { + // #replace needs built-in support for named groups. + // #match works fine because it just return the exec results, even if it has + // a "grops" property. + var re = /./; + re.exec = function () { + var result = []; + result.groups = { a: '7' }; + return result; + }; + return ''.replace(re, '$<a>') !== '7'; + }); + + // IE <= 11 replaces $0 with the whole match, as if it was $& + // https://stackoverflow.com/questions/6024666/getting-ie-to-replace-a-regex-with-the-literal-string-0 + var REPLACE_KEEPS_$0 = (function () { + return 'a'.replace(/./, '$0') === '$0'; + })(); + + // Chrome 51 has a buggy "split" implementation when RegExp#exec !== nativeExec + // Weex JS has frozen built-in prototypes, so use try / catch wrapper + var SPLIT_WORKS_WITH_OVERWRITTEN_EXEC = !fails(function () { + var re = /(?:)/; + var originalExec = re.exec; + re.exec = function () { return originalExec.apply(this, arguments); }; + var result = 'ab'.split(re); + return result.length !== 2 || result[0] !== 'a' || result[1] !== 'b'; + }); + + var fixRegexpWellKnownSymbolLogic = function (KEY, length, exec, sham) { + var SYMBOL = wellKnownSymbol(KEY); + + var DELEGATES_TO_SYMBOL = !fails(function () { + // String methods call symbol-named RegEp methods + var O = {}; + O[SYMBOL] = function () { return 7; }; + return ''[KEY](O) != 7; + }); + + var DELEGATES_TO_EXEC = DELEGATES_TO_SYMBOL && !fails(function () { + // Symbol-named RegExp methods call .exec + var execCalled = false; + var re = /a/; + + if (KEY === 'split') { + // We can't use real regex here since it causes deoptimization + // and serious performance degradation in V8 + // https://github.com/zloirock/core-js/issues/306 + re = {}; + // RegExp[@@split] doesn't call the regex's exec method, but first creates + // a new one. We need to return the patched regex when creating the new one. + re.constructor = {}; + re.constructor[SPECIES$2] = function () { return re; }; + re.flags = ''; + re[SYMBOL] = /./[SYMBOL]; + } + + re.exec = function () { execCalled = true; return null; }; + + re[SYMBOL](''); + return !execCalled; + }); + + if ( + !DELEGATES_TO_SYMBOL || + !DELEGATES_TO_EXEC || + (KEY === 'replace' && !(REPLACE_SUPPORTS_NAMED_GROUPS && REPLACE_KEEPS_$0)) || + (KEY === 'split' && !SPLIT_WORKS_WITH_OVERWRITTEN_EXEC) + ) { + var nativeRegExpMethod = /./[SYMBOL]; + var methods = exec(SYMBOL, ''[KEY], function (nativeMethod, regexp, str, arg2, forceStringMethod) { + if (regexp.exec === regexpExec) { + if (DELEGATES_TO_SYMBOL && !forceStringMethod) { + // The native String method already delegates to @@method (this + // polyfilled function), leasing to infinite recursion. + // We avoid it by directly calling the native @@method method. + return { done: true, value: nativeRegExpMethod.call(regexp, str, arg2) }; + } + return { done: true, value: nativeMethod.call(str, regexp, arg2) }; + } + return { done: false }; + }, { REPLACE_KEEPS_$0: REPLACE_KEEPS_$0 }); + var stringMethod = methods[0]; + var regexMethod = methods[1]; + + redefine(String.prototype, KEY, stringMethod); + redefine(RegExp.prototype, SYMBOL, length == 2 + // 21.2.5.8 RegExp.prototype[@@replace](string, replaceValue) + // 21.2.5.11 RegExp.prototype[@@split](string, limit) + ? function (string, arg) { return regexMethod.call(string, this, arg); } + // 21.2.5.6 RegExp.prototype[@@match](string) + // 21.2.5.9 RegExp.prototype[@@search](string) + : function (string) { return regexMethod.call(string, this); } + ); + } + + if (sham) createNonEnumerableProperty(RegExp.prototype[SYMBOL], 'sham', true); + }; + + // `String.prototype.{ codePointAt, at }` methods implementation + var createMethod$3 = function (CONVERT_TO_STRING) { + return function ($this, pos) { + var S = String(requireObjectCoercible($this)); + var position = toInteger(pos); + var size = S.length; + var first, second; + if (position < 0 || position >= size) return CONVERT_TO_STRING ? '' : undefined; + first = S.charCodeAt(position); + return first < 0xD800 || first > 0xDBFF || position + 1 === size + || (second = S.charCodeAt(position + 1)) < 0xDC00 || second > 0xDFFF + ? CONVERT_TO_STRING ? S.charAt(position) : first + : CONVERT_TO_STRING ? S.slice(position, position + 2) : (first - 0xD800 << 10) + (second - 0xDC00) + 0x10000; + }; + }; + + var stringMultibyte = { + // `String.prototype.codePointAt` method + // https://tc39.github.io/ecma262/#sec-string.prototype.codepointat + codeAt: createMethod$3(false), + // `String.prototype.at` method + // https://github.com/mathiasbynens/String.prototype.at + charAt: createMethod$3(true) + }; + + var charAt = stringMultibyte.charAt; + + // `AdvanceStringIndex` abstract operation + // https://tc39.github.io/ecma262/#sec-advancestringindex + var advanceStringIndex = function (S, index, unicode) { + return index + (unicode ? charAt(S, index).length : 1); + }; + + // `RegExpExec` abstract operation + // https://tc39.github.io/ecma262/#sec-regexpexec + var regexpExecAbstract = function (R, S) { + var exec = R.exec; + if (typeof exec === 'function') { + var result = exec.call(R, S); + if (typeof result !== 'object') { + throw TypeError('RegExp exec method returned something other than an Object or null'); + } + return result; + } + + if (classofRaw(R) !== 'RegExp') { + throw TypeError('RegExp#exec called on incompatible receiver'); + } + + return regexpExec.call(R, S); + }; + + // @@match logic + fixRegexpWellKnownSymbolLogic('match', 1, function (MATCH, nativeMatch, maybeCallNative) { + return [ + // `String.prototype.match` method + // https://tc39.github.io/ecma262/#sec-string.prototype.match + function match(regexp) { + var O = requireObjectCoercible(this); + var matcher = regexp == undefined ? undefined : regexp[MATCH]; + return matcher !== undefined ? matcher.call(regexp, O) : new RegExp(regexp)[MATCH](String(O)); + }, + // `RegExp.prototype[@@match]` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@match + function (regexp) { + var res = maybeCallNative(nativeMatch, regexp, this); + if (res.done) return res.value; + + var rx = anObject(regexp); + var S = String(this); + + if (!rx.global) return regexpExecAbstract(rx, S); + + var fullUnicode = rx.unicode; + rx.lastIndex = 0; + var A = []; + var n = 0; + var result; + while ((result = regexpExecAbstract(rx, S)) !== null) { + var matchStr = String(result[0]); + A[n] = matchStr; + if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode); + n++; + } + return n === 0 ? null : A; + } + ]; + }); + + var max$1 = Math.max; + var min$2 = Math.min; + var floor$1 = Math.floor; + var SUBSTITUTION_SYMBOLS = /\$([$&'`]|\d\d?|<[^>]*>)/g; + var SUBSTITUTION_SYMBOLS_NO_NAMED = /\$([$&'`]|\d\d?)/g; + + var maybeToString = function (it) { + return it === undefined ? it : String(it); + }; + + // @@replace logic + fixRegexpWellKnownSymbolLogic('replace', 2, function (REPLACE, nativeReplace, maybeCallNative, reason) { + return [ + // `String.prototype.replace` method + // https://tc39.github.io/ecma262/#sec-string.prototype.replace + function replace(searchValue, replaceValue) { + var O = requireObjectCoercible(this); + var replacer = searchValue == undefined ? undefined : searchValue[REPLACE]; + return replacer !== undefined + ? replacer.call(searchValue, O, replaceValue) + : nativeReplace.call(String(O), searchValue, replaceValue); + }, + // `RegExp.prototype[@@replace]` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@replace + function (regexp, replaceValue) { + if (reason.REPLACE_KEEPS_$0 || (typeof replaceValue === 'string' && replaceValue.indexOf('$0') === -1)) { + var res = maybeCallNative(nativeReplace, regexp, this, replaceValue); + if (res.done) return res.value; + } + + var rx = anObject(regexp); + var S = String(this); + + var functionalReplace = typeof replaceValue === 'function'; + if (!functionalReplace) replaceValue = String(replaceValue); + + var global = rx.global; + if (global) { + var fullUnicode = rx.unicode; + rx.lastIndex = 0; + } + var results = []; + while (true) { + var result = regexpExecAbstract(rx, S); + if (result === null) break; + + results.push(result); + if (!global) break; + + var matchStr = String(result[0]); + if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode); + } + + var accumulatedResult = ''; + var nextSourcePosition = 0; + for (var i = 0; i < results.length; i++) { + result = results[i]; + + var matched = String(result[0]); + var position = max$1(min$2(toInteger(result.index), S.length), 0); + var captures = []; + // NOTE: This is equivalent to + // captures = result.slice(1).map(maybeToString) + // but for some reason `nativeSlice.call(result, 1, result.length)` (called in + // the slice polyfill when slicing native arrays) "doesn't work" in safari 9 and + // causes a crash (https://pastebin.com/N21QzeQA) when trying to debug it. + for (var j = 1; j < result.length; j++) captures.push(maybeToString(result[j])); + var namedCaptures = result.groups; + if (functionalReplace) { + var replacerArgs = [matched].concat(captures, position, S); + if (namedCaptures !== undefined) replacerArgs.push(namedCaptures); + var replacement = String(replaceValue.apply(undefined, replacerArgs)); + } else { + replacement = getSubstitution(matched, S, position, captures, namedCaptures, replaceValue); + } + if (position >= nextSourcePosition) { + accumulatedResult += S.slice(nextSourcePosition, position) + replacement; + nextSourcePosition = position + matched.length; + } + } + return accumulatedResult + S.slice(nextSourcePosition); + } + ]; + + // https://tc39.github.io/ecma262/#sec-getsubstitution + function getSubstitution(matched, str, position, captures, namedCaptures, replacement) { + var tailPos = position + matched.length; + var m = captures.length; + var symbols = SUBSTITUTION_SYMBOLS_NO_NAMED; + if (namedCaptures !== undefined) { + namedCaptures = toObject(namedCaptures); + symbols = SUBSTITUTION_SYMBOLS; + } + return nativeReplace.call(replacement, symbols, function (match, ch) { + var capture; + switch (ch.charAt(0)) { + case '$': return '$'; + case '&': return matched; + case '`': return str.slice(0, position); + case "'": return str.slice(tailPos); + case '<': + capture = namedCaptures[ch.slice(1, -1)]; + break; + default: // \d\d? + var n = +ch; + if (n === 0) return match; + if (n > m) { + var f = floor$1(n / 10); + if (f === 0) return match; + if (f <= m) return captures[f - 1] === undefined ? ch.charAt(1) : captures[f - 1] + ch.charAt(1); + return match; + } + capture = captures[n - 1]; + } + return capture === undefined ? '' : capture; + }); + } + }); + + var SPECIES$3 = wellKnownSymbol('species'); + + // `SpeciesConstructor` abstract operation + // https://tc39.github.io/ecma262/#sec-speciesconstructor + var speciesConstructor = function (O, defaultConstructor) { + var C = anObject(O).constructor; + var S; + return C === undefined || (S = anObject(C)[SPECIES$3]) == undefined ? defaultConstructor : aFunction$1(S); + }; + + var arrayPush = [].push; + var min$3 = Math.min; + var MAX_UINT32 = 0xFFFFFFFF; + + // babel-minify transpiles RegExp('x', 'y') -> /x/y and it causes SyntaxError + var SUPPORTS_Y = !fails(function () { return !RegExp(MAX_UINT32, 'y'); }); + + // @@split logic + fixRegexpWellKnownSymbolLogic('split', 2, function (SPLIT, nativeSplit, maybeCallNative) { + var internalSplit; + if ( + 'abbc'.split(/(b)*/)[1] == 'c' || + 'test'.split(/(?:)/, -1).length != 4 || + 'ab'.split(/(?:ab)*/).length != 2 || + '.'.split(/(.?)(.?)/).length != 4 || + '.'.split(/()()/).length > 1 || + ''.split(/.?/).length + ) { + // based on es5-shim implementation, need to rework it + internalSplit = function (separator, limit) { + var string = String(requireObjectCoercible(this)); + var lim = limit === undefined ? MAX_UINT32 : limit >>> 0; + if (lim === 0) return []; + if (separator === undefined) return [string]; + // If `separator` is not a regex, use native split + if (!isRegexp(separator)) { + return nativeSplit.call(string, separator, lim); + } + var output = []; + var flags = (separator.ignoreCase ? 'i' : '') + + (separator.multiline ? 'm' : '') + + (separator.unicode ? 'u' : '') + + (separator.sticky ? 'y' : ''); + var lastLastIndex = 0; + // Make `global` and avoid `lastIndex` issues by working with a copy + var separatorCopy = new RegExp(separator.source, flags + 'g'); + var match, lastIndex, lastLength; + while (match = regexpExec.call(separatorCopy, string)) { + lastIndex = separatorCopy.lastIndex; + if (lastIndex > lastLastIndex) { + output.push(string.slice(lastLastIndex, match.index)); + if (match.length > 1 && match.index < string.length) arrayPush.apply(output, match.slice(1)); + lastLength = match[0].length; + lastLastIndex = lastIndex; + if (output.length >= lim) break; + } + if (separatorCopy.lastIndex === match.index) separatorCopy.lastIndex++; // Avoid an infinite loop + } + if (lastLastIndex === string.length) { + if (lastLength || !separatorCopy.test('')) output.push(''); + } else output.push(string.slice(lastLastIndex)); + return output.length > lim ? output.slice(0, lim) : output; + }; + // Chakra, V8 + } else if ('0'.split(undefined, 0).length) { + internalSplit = function (separator, limit) { + return separator === undefined && limit === 0 ? [] : nativeSplit.call(this, separator, limit); + }; + } else internalSplit = nativeSplit; + + return [ + // `String.prototype.split` method + // https://tc39.github.io/ecma262/#sec-string.prototype.split + function split(separator, limit) { + var O = requireObjectCoercible(this); + var splitter = separator == undefined ? undefined : separator[SPLIT]; + return splitter !== undefined + ? splitter.call(separator, O, limit) + : internalSplit.call(String(O), separator, limit); + }, + // `RegExp.prototype[@@split]` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@split + // + // NOTE: This cannot be properly polyfilled in engines that don't support + // the 'y' flag. + function (regexp, limit) { + var res = maybeCallNative(internalSplit, regexp, this, limit, internalSplit !== nativeSplit); + if (res.done) return res.value; + + var rx = anObject(regexp); + var S = String(this); + var C = speciesConstructor(rx, RegExp); + + var unicodeMatching = rx.unicode; + var flags = (rx.ignoreCase ? 'i' : '') + + (rx.multiline ? 'm' : '') + + (rx.unicode ? 'u' : '') + + (SUPPORTS_Y ? 'y' : 'g'); + + // ^(? + rx + ) is needed, in combination with some S slicing, to + // simulate the 'y' flag. + var splitter = new C(SUPPORTS_Y ? rx : '^(?:' + rx.source + ')', flags); + var lim = limit === undefined ? MAX_UINT32 : limit >>> 0; + if (lim === 0) return []; + if (S.length === 0) return regexpExecAbstract(splitter, S) === null ? [S] : []; + var p = 0; + var q = 0; + var A = []; + while (q < S.length) { + splitter.lastIndex = SUPPORTS_Y ? q : 0; + var z = regexpExecAbstract(splitter, SUPPORTS_Y ? S : S.slice(q)); + var e; + if ( + z === null || + (e = min$3(toLength(splitter.lastIndex + (SUPPORTS_Y ? 0 : q)), S.length)) === p + ) { + q = advanceStringIndex(S, q, unicodeMatching); + } else { + A.push(S.slice(p, q)); + if (A.length === lim) return A; + for (var i = 1; i <= z.length - 1; i++) { + A.push(z[i]); + if (A.length === lim) return A; + } + q = p = e; + } + } + A.push(S.slice(p)); + return A; + } + ]; + }, !SUPPORTS_Y); + + var Utils = $.fn.bootstrapTable.utils; + var searchControls = 'select, input:not([type="checkbox"]):not([type="radio"])'; + function getOptionsFromSelectControl(selectControl) { + return selectControl.get(selectControl.length - 1).options; + } + function getControlContainer(that) { + if (that.options.filterControlContainer) { + return $("".concat(that.options.filterControlContainer)); + } + + return that.$header; + } + function getSearchControls(that) { + return getControlContainer(that).find(searchControls); + } + function hideUnusedSelectOptions(selectControl, uniqueValues) { + var options = getOptionsFromSelectControl(selectControl); + + for (var i = 0; i < options.length; i++) { + if (options[i].value !== '') { + if (!uniqueValues.hasOwnProperty(options[i].value)) { + selectControl.find(Utils.sprintf('option[value=\'%s\']', options[i].value)).hide(); + } else { + selectControl.find(Utils.sprintf('option[value=\'%s\']', options[i].value)).show(); + } + } + } + } + function existOptionInSelectControl(selectControl, value) { + var options = getOptionsFromSelectControl(selectControl); + + for (var i = 0; i < options.length; i++) { + if (options[i].value === value.toString()) { + // The value is not valid to add + return true; + } + } // If we get here, the value is valid to add + + + return false; + } + function addOptionToSelectControl(selectControl, _value, text, selected) { + var value = _value === undefined || _value === null ? '' : _value.toString().trim(); + var $selectControl = $(selectControl.get(selectControl.length - 1)); + + if (!existOptionInSelectControl(selectControl, value)) { + var option = $("<option value=\"".concat(value, "\">").concat(text, "</option>")); + + if (value === selected) { + option.attr('selected', true); + } + + $selectControl.append(option); + } + } + function sortSelectControl(selectControl, orderBy) { + var $selectControl = $(selectControl.get(selectControl.length - 1)); + var $opts = $selectControl.find('option:gt(0)'); + + if (orderBy !== 'server') { + $opts.sort(function (a, b) { + return Utils.sort(a.textContent, b.textContent, orderBy === 'desc' ? -1 : 1); + }); + } + + $selectControl.find('option:gt(0)').remove(); + $selectControl.append($opts); + } + function fixHeaderCSS(_ref) { + var $tableHeader = _ref.$tableHeader; + $tableHeader.css('height', '89px'); + } + function getElementClass($element) { + return $element.attr('class').replace('form-control', '').replace('focus-temp', '').replace('search-input', '').trim(); + } + function getCursorPosition(el) { + if (Utils.isIEBrowser()) { + if ($(el).is('input[type=text]')) { + var pos = 0; + + if ('selectionStart' in el) { + pos = el.selectionStart; + } else if ('selection' in document) { + el.focus(); + var Sel = document.selection.createRange(); + var SelLength = document.selection.createRange().text.length; + Sel.moveStart('character', -el.value.length); + pos = Sel.text.length - SelLength; + } + + return pos; + } + + return -1; + } + + return -1; + } + function setCursorPosition(el) { + $(el).val(el.value); + } + function copyValues(that) { + var searchControls = getSearchControls(that); + that.options.valuesFilterControl = []; + searchControls.each(function () { + var $field = $(this); + + if (that.options.height) { + var fieldClass = getElementClass($field); + $field = $(".fixed-table-header .".concat(fieldClass)); + } + + that.options.valuesFilterControl.push({ + field: $field.closest('[data-field]').data('field'), + value: $field.val(), + position: getCursorPosition($field.get(0)), + hasFocus: $field.is(':focus') + }); + }); + } + function setValues(that) { + var field = null; + var result = []; + var searchControls = getSearchControls(that); + + if (that.options.valuesFilterControl.length > 0) { + // Callback to apply after settings fields values + var fieldToFocusCallback = null; + searchControls.each(function (index, ele) { + var $this = $(this); + field = $this.closest('[data-field]').data('field'); + result = that.options.valuesFilterControl.filter(function (valueObj) { + return valueObj.field === field; + }); + + if (result.length > 0) { + if ($this.is('[type=radio]')) { + return; + } + + $this.val(result[0].value); + + if (result[0].hasFocus && result[0].value !== '') { + // set callback if the field had the focus. + fieldToFocusCallback = function (fieldToFocus, carretPosition) { + // Closure here to capture the field and cursor position + var closedCallback = function closedCallback() { + fieldToFocus.focus(); + setCursorPosition(fieldToFocus); + }; + + return closedCallback; + }($this.get(0), result[0].position); + } + } + }); // Callback call. + + if (fieldToFocusCallback !== null) { + fieldToFocusCallback(); + } + } + } + function collectBootstrapCookies() { + var cookies = []; + var foundCookies = document.cookie.match(/(?:bs.table.)(\w*)/g); + var foundLocalStorage = localStorage; + + if (foundCookies) { + $.each(foundCookies, function (i, _cookie) { + var cookie = _cookie; + + if (/./.test(cookie)) { + cookie = cookie.split('.').pop(); + } + + if ($.inArray(cookie, cookies) === -1) { + cookies.push(cookie); + } + }); + } + + if (foundLocalStorage) { + for (var i = 0; i < foundLocalStorage.length; i++) { + var cookie = foundLocalStorage.key(i); + + if (/./.test(cookie)) { + cookie = cookie.split('.').pop(); + } + + if (!cookies.includes(cookie)) { + cookies.push(cookie); + } + } + } + + return cookies; + } + function escapeID(id) { + // eslint-disable-next-line no-useless-escape + return String(id).replace(/([:.\[\],])/g, '\\$1'); + } + function isColumnSearchableViaSelect(_ref2) { + var filterControl = _ref2.filterControl, + searchable = _ref2.searchable; + return filterControl && filterControl.toLowerCase() === 'select' && searchable; + } + function isFilterDataNotGiven(_ref3) { + var filterData = _ref3.filterData; + return filterData === undefined || filterData.toLowerCase() === 'column'; + } + function hasSelectControlElement(selectControl) { + return selectControl && selectControl.length > 0; + } + function initFilterSelectControls(that) { + var data = that.data; + var z = that.options.pagination ? that.options.sidePagination === 'server' ? that.pageTo : that.options.totalRows : that.pageTo; + $.each(that.header.fields, function (j, field) { + var column = that.columns[that.fieldsColumnsIndex[field]]; + var selectControl = getControlContainer(that).find("select.bootstrap-table-filter-control-".concat(escapeID(column.field))); + + if (isColumnSearchableViaSelect(column) && isFilterDataNotGiven(column) && hasSelectControlElement(selectControl)) { + if (selectControl.get(selectControl.length - 1).options.length === 0) { + // Added the default option + addOptionToSelectControl(selectControl, '', column.filterControlPlaceholder, column.filterDefault); + } + + var uniqueValues = {}; + + for (var i = 0; i < z; i++) { + // Added a new value + var fieldValue = data[i][field]; + var formatter = that.options.editable && column.editable ? column._formatter : that.header.formatters[j]; + var formattedValue = Utils.calculateObjectValue(that.header, formatter, [fieldValue, data[i], i], fieldValue); + + if (column.filterDataCollector) { + formattedValue = Utils.calculateObjectValue(that.header, column.filterDataCollector, [fieldValue, data[i], formattedValue], formattedValue); + } + + if (column.searchFormatter) { + fieldValue = formattedValue; + } + + uniqueValues[formattedValue] = fieldValue; + + if (_typeof(formattedValue) === 'object' && formattedValue !== null) { + formattedValue.forEach(function (value) { + addOptionToSelectControl(selectControl, value, value, column.filterDefault); + }); + continue; + } + + for (var key in uniqueValues) { + addOptionToSelectControl(selectControl, uniqueValues[key], key, column.filterDefault); + } + } + + sortSelectControl(selectControl, column.filterOrderBy); + + if (that.options.hideUnusedSelectOptions) { + hideUnusedSelectOptions(selectControl, uniqueValues); + } + } + }); + } + function getFilterDataMethod(objFilterDataMethod, searchTerm) { + var keys = Object.keys(objFilterDataMethod); + + for (var i = 0; i < keys.length; i++) { + if (keys[i] === searchTerm) { + return objFilterDataMethod[searchTerm]; + } + } + + return null; + } + function createControls(that, header) { + var addedFilterControl = false; + var html; + $.each(that.columns, function (_, column) { + html = []; + + if (!column.visible) { + return; + } + + if (!column.filterControl && !that.options.filterControlContainer) { + html.push('<div class="no-filter-control"></div>'); + } else if (that.options.filterControlContainer) { + var $filterControls = $(".bootstrap-table-filter-control-".concat(column.field)); + $.each($filterControls, function (_, filterControl) { + var $filterControl = $(filterControl); + + if (!$filterControl.is('[type=radio]')) { + var placeholder = column.filterControlPlaceholder ? column.filterControlPlaceholder : ''; + $filterControl.attr('placeholder', placeholder).val(column.filterDefault); + } + + $filterControl.attr('data-field', column.field); + }); + addedFilterControl = true; + } else { + var nameControl = column.filterControl.toLowerCase(); + html.push('<div class="filter-control">'); + addedFilterControl = true; + + if (column.searchable && that.options.filterTemplate[nameControl]) { + html.push(that.options.filterTemplate[nameControl](that, column.field, column.filterControlPlaceholder ? column.filterControlPlaceholder : '', column.filterDefault)); + } + } + + if (!column.filterControl && '' !== column.filterDefault && 'undefined' !== typeof column.filterDefault) { + if ($.isEmptyObject(that.filterColumnsPartial)) { + that.filterColumnsPartial = {}; + } + + that.filterColumnsPartial[column.field] = column.filterDefault; + } + + $.each(header.find('th'), function (i, th) { + var $th = $(th); + + if ($th.data('field') === column.field) { + $th.find('.fht-cell').append(html.join('')); + return false; + } + }); + + if (column.filterData && column.filterData.toLowerCase() !== 'column') { + var filterDataType = getFilterDataMethod( + /* eslint-disable no-use-before-define */ + filterDataMethods, column.filterData.substring(0, column.filterData.indexOf(':'))); + var filterDataSource; + var selectControl; + + if (filterDataType) { + filterDataSource = column.filterData.substring(column.filterData.indexOf(':') + 1, column.filterData.length); + selectControl = header.find(".bootstrap-table-filter-control-".concat(escapeID(column.field))); + addOptionToSelectControl(selectControl, '', column.filterControlPlaceholder, column.filterDefault); + filterDataType(filterDataSource, selectControl, that.options.filterOrderBy, column.filterDefault); + } else { + throw new SyntaxError('Error. You should use any of these allowed filter data methods: var, obj, json, url, func.' + ' Use like this: var: {key: "value"}'); + } + } + }); + + if (addedFilterControl) { + header.off('keyup', 'input').on('keyup', 'input', function (_ref4, obj) { + var currentTarget = _ref4.currentTarget, + keyCode = _ref4.keyCode; + syncControls(that); // Simulate enter key action from clear button + + keyCode = obj ? obj.keyCode : keyCode; + + if (that.options.searchOnEnterKey && keyCode !== 13) { + return; + } + + if ($.inArray(keyCode, [37, 38, 39, 40]) > -1) { + return; + } + + var $currentTarget = $(currentTarget); + + if ($currentTarget.is(':checkbox') || $currentTarget.is(':radio')) { + return; + } + + clearTimeout(currentTarget.timeoutId || 0); + currentTarget.timeoutId = setTimeout(function () { + that.onColumnSearch({ + currentTarget: currentTarget, + keyCode: keyCode + }); + }, that.options.searchTimeOut); + }); + header.off('change', 'select:not(".ms-offscreen")').on('change', 'select:not(".ms-offscreen")', function (_ref5) { + var currentTarget = _ref5.currentTarget, + keyCode = _ref5.keyCode; + syncControls(that); + var $select = $(currentTarget); + var value = $select.val(); + + if (value && value.length > 0 && value.trim()) { + $select.find('option[selected]').removeAttr('selected'); + $select.find('option[value="' + value + '"]').attr('selected', true); + } else { + $select.find('option[selected]').removeAttr('selected'); + } + + clearTimeout(currentTarget.timeoutId || 0); + currentTarget.timeoutId = setTimeout(function () { + that.onColumnSearch({ + currentTarget: currentTarget, + keyCode: keyCode + }); + }, that.options.searchTimeOut); + }); + header.off('mouseup', 'input:not([type=radio])').on('mouseup', 'input:not([type=radio])', function (_ref6) { + var currentTarget = _ref6.currentTarget, + keyCode = _ref6.keyCode; + var $input = $(currentTarget); + var oldValue = $input.val(); + + if (oldValue === '') { + return; + } + + setTimeout(function () { + syncControls(that); + var newValue = $input.val(); + + if (newValue === '') { + clearTimeout(currentTarget.timeoutId || 0); + currentTarget.timeoutId = setTimeout(function () { + that.onColumnSearch({ + currentTarget: currentTarget, + keyCode: keyCode + }); + }, that.options.searchTimeOut); + } + }, 1); + }); + header.off('change', 'input[type=radio]').on('change', 'input[type=radio]', function (_ref7) { + var currentTarget = _ref7.currentTarget, + keyCode = _ref7.keyCode; + clearTimeout(currentTarget.timeoutId || 0); + currentTarget.timeoutId = setTimeout(function () { + syncControls(that); + that.onColumnSearch({ + currentTarget: currentTarget, + keyCode: keyCode + }); + }, that.options.searchTimeOut); + }); + + if (header.find('.date-filter-control').length > 0) { + $.each(that.columns, function (i, _ref8) { + var filterControl = _ref8.filterControl, + field = _ref8.field, + filterDatepickerOptions = _ref8.filterDatepickerOptions; + + if (filterControl !== undefined && filterControl.toLowerCase() === 'datepicker') { + header.find(".date-filter-control.bootstrap-table-filter-control-".concat(field)).datepicker(filterDatepickerOptions).on('changeDate', function (_ref9) { + var currentTarget = _ref9.currentTarget, + keyCode = _ref9.keyCode; + clearTimeout(currentTarget.timeoutId || 0); + currentTarget.timeoutId = setTimeout(function () { + syncControls(that); + that.onColumnSearch({ + currentTarget: currentTarget, + keyCode: keyCode + }); + }, that.options.searchTimeOut); + }); + } + }); + } + + if (that.options.sidePagination !== 'server' && !that.options.height) { + that.triggerSearch(); + } + + if (!that.options.filterControlVisible) { + header.find('.filter-control, .no-filter-control').hide(); + } + } else { + header.find('.filter-control, .no-filter-control').hide(); + } + + that.trigger('created-controls'); + } + function getDirectionOfSelectOptions(_alignment) { + var alignment = _alignment === undefined ? 'left' : _alignment.toLowerCase(); + + switch (alignment) { + case 'left': + return 'ltr'; + + case 'right': + return 'rtl'; + + case 'auto': + return 'auto'; + + default: + return 'ltr'; + } + } + function syncControls(that) { + if (that.options.height) { + var controlsTableHeader = that.$tableHeader.find(searchControls); + that.$header.find(searchControls).each(function (_, control) { + var $control = $(control); + var controlClass = getElementClass($control); + var foundControl = controlsTableHeader.filter(function (_, ele) { + var eleClass = getElementClass($(ele)); + return controlClass === eleClass; + }); + + if (foundControl.length === 0) { + return; + } + + if ($control.is('select')) { + $control.find('option:selected').removeAttr('selected'); + $control.find("option[value='".concat(foundControl.val(), "']")).attr('selected', true); + } else { + $control.val(foundControl.val()); + } + }); + } + } + var filterDataMethods = { + func: function func(filterDataSource, selectControl, filterOrderBy, selected) { + var variableValues = window[filterDataSource].apply(); + + for (var key in variableValues) { + addOptionToSelectControl(selectControl, key, variableValues[key], selected); + } + + sortSelectControl(selectControl, filterOrderBy); + }, + obj: function obj(filterDataSource, selectControl, filterOrderBy, selected) { + var objectKeys = filterDataSource.split('.'); + var variableName = objectKeys.shift(); + var variableValues = window[variableName]; + + if (objectKeys.length > 0) { + objectKeys.forEach(function (key) { + variableValues = variableValues[key]; + }); + } + + for (var key in variableValues) { + addOptionToSelectControl(selectControl, key, variableValues[key], selected); + } + + sortSelectControl(selectControl, filterOrderBy); + }, + var: function _var(filterDataSource, selectControl, filterOrderBy, selected) { + var variableValues = window[filterDataSource]; + var isArray = Array.isArray(variableValues); + + for (var key in variableValues) { + if (isArray) { + addOptionToSelectControl(selectControl, variableValues[key], variableValues[key], selected); + } else { + addOptionToSelectControl(selectControl, key, variableValues[key], selected); + } + } + + sortSelectControl(selectControl, filterOrderBy); + }, + url: function url(filterDataSource, selectControl, filterOrderBy, selected) { + $.ajax({ + url: filterDataSource, + dataType: 'json', + success: function success(data) { + for (var key in data) { + addOptionToSelectControl(selectControl, key, data[key], selected); + } + + sortSelectControl(selectControl, filterOrderBy); + } + }); + }, + json: function json(filterDataSource, selectControl, filterOrderBy, selected) { + var variableValues = JSON.parse(filterDataSource); + + for (var key in variableValues) { + addOptionToSelectControl(selectControl, key, variableValues[key], selected); + } + + sortSelectControl(selectControl, filterOrderBy); + } + }; + + var Utils$1 = $.fn.bootstrapTable.utils; + $.extend($.fn.bootstrapTable.defaults, { + filterControl: false, + filterControlVisible: true, + onColumnSearch: function onColumnSearch(field, text) { + return false; + }, + onCreatedControls: function onCreatedControls() { + return false; + }, + alignmentSelectControlOptions: undefined, + filterTemplate: { + input: function input(that, field, placeholder, value) { + return Utils$1.sprintf('<input type="search" class="form-control bootstrap-table-filter-control-%s search-input" style="width: 100%;" placeholder="%s" value="%s">', field, 'undefined' === typeof placeholder ? '' : placeholder, 'undefined' === typeof value ? '' : value); + }, + select: function select(_ref, field) { + var options = _ref.options; + return Utils$1.sprintf('<select class="form-control bootstrap-table-filter-control-%s" style="width: 100%;" dir="%s"></select>', field, getDirectionOfSelectOptions(options.alignmentSelectControlOptions)); + }, + datepicker: function datepicker(that, field, value) { + return Utils$1.sprintf('<input type="text" class="form-control date-filter-control bootstrap-table-filter-control-%s" style="width: 100%;" value="%s">', field, 'undefined' === typeof value ? '' : value); + } + }, + disableControlWhenSearch: false, + searchOnEnterKey: false, + showFilterControlSwitch: false, + // internal variables + valuesFilterControl: [] + }); + $.extend($.fn.bootstrapTable.columnDefaults, { + filterControl: undefined, + // input, select, datepicker + filterDataCollector: undefined, + filterData: undefined, + filterDatepickerOptions: undefined, + filterStrictSearch: false, + filterStartsWithSearch: false, + filterControlPlaceholder: '', + filterDefault: '', + filterOrderBy: 'asc' // asc || desc + + }); + $.extend($.fn.bootstrapTable.Constructor.EVENTS, { + 'column-search.bs.table': 'onColumnSearch', + 'created-controls.bs.table': 'onCreatedControls' + }); + $.extend($.fn.bootstrapTable.defaults.icons, { + clear: { + bootstrap3: 'glyphicon-trash icon-clear' + }[$.fn.bootstrapTable.theme] || 'fa-trash', + filterControlSwitchHide: { + bootstrap3: 'glyphicon-zoom-out icon-zoom-out', + materialize: 'zoom_out' + }[$.fn.bootstrapTable.theme] || 'fa-search-minus', + filterControlSwitchShow: { + bootstrap3: 'glyphicon-zoom-in icon-zoom-in', + materialize: 'zoom_in' + }[$.fn.bootstrapTable.theme] || 'fa-search-plus' + }); + $.extend($.fn.bootstrapTable.locales, { + formatFilterControlSwitch: function formatFilterControlSwitch() { + return 'Hide/Show controls'; + }, + formatFilterControlSwitchHide: function formatFilterControlSwitchHide() { + return 'Hide controls'; + }, + formatFilterControlSwitchShow: function formatFilterControlSwitchShow() { + return 'Show controls'; + } + }); + $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales); + $.extend($.fn.bootstrapTable.defaults, { + formatClearSearch: function formatClearSearch() { + return 'Clear filters'; + } + }); + $.fn.bootstrapTable.methods.push('triggerSearch'); + $.fn.bootstrapTable.methods.push('clearFilterControl'); + $.fn.bootstrapTable.methods.push('toggleFilterControl'); + + $.BootstrapTable = + /*#__PURE__*/ + function (_$$BootstrapTable) { + _inherits(_class, _$$BootstrapTable); + + function _class() { + _classCallCheck(this, _class); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class).apply(this, arguments)); + } + + _createClass(_class, [{ + key: "init", + value: function init() { + var _this = this; + + // Make sure that the filterControl option is set + if (this.options.filterControl) { + // Make sure that the internal variables are set correctly + this.options.valuesFilterControl = []; + this.$el.on('reset-view.bs.table', function () { + // Create controls on $tableHeader if the height is set + if (!_this.options.height) { + return; + } // Avoid recreate the controls + + + var $controlContainer = getControlContainer(_this); + + if ($controlContainer.find('select').length > 0 || $controlContainer.find('input:not([type="checkbox"]):not([type="radio"])').length > 0) { + return; + } + + createControls(_this, $controlContainer); + }).on('post-header.bs.table', function () { + setValues(_this); + }).on('post-body.bs.table', function () { + if (_this.options.height && !_this.options.filterControlContainer) { + fixHeaderCSS(_this); + } + + _this.$tableLoading.css('top', _this.$header.outerHeight() + 1); + }).on('column-switch.bs.table', function () { + setValues(_this); + }).on('load-success.bs.table', function () { + _this.enableControls(true); + }).on('load-error.bs.table', function () { + _this.enableControls(true); + }); + } + + _get(_getPrototypeOf(_class.prototype), "init", this).call(this); + } + }, { + key: "initHeader", + value: function initHeader() { + _get(_getPrototypeOf(_class.prototype), "initHeader", this).call(this); + + if (!this.options.filterControl || this.options.height) { + return; + } + + createControls(this, getControlContainer(this)); + } + }, { + key: "initBody", + value: function initBody() { + _get(_getPrototypeOf(_class.prototype), "initBody", this).call(this); + + syncControls(this); + initFilterSelectControls(this); + } + }, { + key: "initSearch", + value: function initSearch() { + var _this2 = this; + + var that = this; + var fp = $.isEmptyObject(that.filterColumnsPartial) ? null : that.filterColumnsPartial; + + _get(_getPrototypeOf(_class.prototype), "initSearch", this).call(this); + + if (this.options.sidePagination === 'server' || fp === null) { + return; + } // Check partial column filter + + + that.data = fp ? that.data.filter(function (item, i) { + var itemIsExpected = []; + var keys1 = Object.keys(item); + var keys2 = Object.keys(fp); + var keys = keys1.concat(keys2.filter(function (item) { + return !keys1.includes(item); + })); + keys.forEach(function (key) { + var thisColumn = that.columns[that.fieldsColumnsIndex[key]]; + var fval = (fp[key] || '').toLowerCase(); + var value = Utils$1.getItemField(item, key, false); + var tmpItemIsExpected; + + if (fval === '') { + tmpItemIsExpected = true; + } else { + // Fix #142: search use formatted data + if (thisColumn && thisColumn.searchFormatter) { + value = $.fn.bootstrapTable.utils.calculateObjectValue(that.header, that.header.formatters[$.inArray(key, that.header.fields)], [value, item, i], value); + } + + if ($.inArray(key, that.header.fields) !== -1) { + if (value === undefined || value === null) { + tmpItemIsExpected = false; + } else if (_typeof(value) === 'object') { + value.forEach(function (objectValue) { + if (tmpItemIsExpected) { + return; + } + + if (_this2.options.searchAccentNeutralise) { + objectValue = Utils$1.normalizeAccent(objectValue); + } + + tmpItemIsExpected = that.isValueExpected(fval, objectValue, thisColumn, key); + }); + } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + if (_this2.options.searchAccentNeutralise) { + value = Utils$1.normalizeAccent(value); + } + + tmpItemIsExpected = that.isValueExpected(fval, value, thisColumn, key); + } + } + } + + itemIsExpected.push(tmpItemIsExpected); + }); + return !itemIsExpected.includes(false); + }) : that.data; + that.unsortedData = _toConsumableArray(that.data); + } + }, { + key: "isValueExpected", + value: function isValueExpected(searchValue, value, column, key) { + var tmpItemIsExpected = false; + + if (column.filterStrictSearch) { + tmpItemIsExpected = value.toString().toLowerCase() === searchValue.toString().toLowerCase(); + } else if (column.filterStartsWithSearch) { + tmpItemIsExpected = "".concat(value).toLowerCase().indexOf(searchValue) === 0; + } else { + tmpItemIsExpected = "".concat(value).toLowerCase().includes(searchValue); + } + + var largerSmallerEqualsRegex = /(?:(<=|=>|=<|>=|>|<)(?:\s+)?(\d+)?|(\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm; + var matches = largerSmallerEqualsRegex.exec(searchValue); + + if (matches) { + var operator = matches[1] || "".concat(matches[5], "l"); + var comparisonValue = matches[2] || matches[3]; + var int = parseInt(value, 10); + var comparisonInt = parseInt(comparisonValue, 10); + + switch (operator) { + case '>': + case '<l': + tmpItemIsExpected = int > comparisonInt; + break; + + case '<': + case '>l': + tmpItemIsExpected = int < comparisonInt; + break; + + case '<=': + case '=<': + case '>=l': + case '=>l': + tmpItemIsExpected = int <= comparisonInt; + break; + + case '>=': + case '=>': + case '<=l': + case '=<l': + tmpItemIsExpected = int >= comparisonInt; + break; + } + } + + if (column.filterCustomSearch) { + var customSearchResult = Utils$1.calculateObjectValue(this, column.filterCustomSearch, [searchValue, value, key, this.options.data], true); + + if (customSearchResult !== null) { + tmpItemIsExpected = customSearchResult; + } + } + + return tmpItemIsExpected; + } + }, { + key: "initColumnSearch", + value: function initColumnSearch(filterColumnsDefaults) { + copyValues(this); + + if (filterColumnsDefaults) { + this.filterColumnsPartial = filterColumnsDefaults; + this.updatePagination(); + + for (var filter in filterColumnsDefaults) { + this.trigger('column-search', filter, filterColumnsDefaults[filter]); + } + } + } + }, { + key: "onColumnSearch", + value: function onColumnSearch(_ref2) { + var currentTarget = _ref2.currentTarget, + keyCode = _ref2.keyCode; + + if ($.inArray(keyCode, [37, 38, 39, 40]) > -1) { + return; + } + + copyValues(this); + var text = $.trim($(currentTarget).val()); + var $field = $(currentTarget).closest('[data-field]').data('field'); + this.trigger('column-search', $field, text); + + if ($.isEmptyObject(this.filterColumnsPartial)) { + this.filterColumnsPartial = {}; + } + + if (text) { + this.filterColumnsPartial[$field] = text; + } else { + delete this.filterColumnsPartial[$field]; + } + + this.options.pageNumber = 1; + this.enableControls(false); + this.onSearch({ + currentTarget: currentTarget + }, false); + } + }, { + key: "initToolbar", + value: function initToolbar() { + this.showToolbar = this.showToolbar || this.options.showFilterControlSwitch; + this.showSearchClearButton = this.options.filterControl && this.options.showSearchClearButton; + + if (this.options.showFilterControlSwitch) { + this.buttons = Object.assign(this.buttons, { + filterControlSwitch: { + 'text': this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow(), + 'icon': this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow, + 'event': this.toggleFilterControl, + 'attributes': { + 'aria-label': this.options.formatFilterControlSwitch(), + 'title': this.options.formatFilterControlSwitch() + } + } + }); + } + + _get(_getPrototypeOf(_class.prototype), "initToolbar", this).call(this); + } + }, { + key: "resetSearch", + value: function resetSearch(text) { + if (this.options.filterControl && this.options.showSearchClearButton) { + this.clearFilterControl(); + } + + _get(_getPrototypeOf(_class.prototype), "resetSearch", this).call(this, text); + } + }, { + key: "clearFilterControl", + value: function clearFilterControl() { + if (this.options.filterControl) { + var that = this; + var cookies = collectBootstrapCookies(); + var table = this.$el.closest('table'); + var controls = getSearchControls(that); + var search = Utils$1.getSearchInput(this); + var hasValues = false; + var timeoutId = 0; + $.each(that.options.valuesFilterControl, function (i, item) { + hasValues = hasValues ? true : item.value !== ''; + item.value = ''; + }); + $.each(that.options.filterControls, function (i, item) { + item.text = ''; + }); + setValues(that); // clear cookies once the filters are clean + + clearTimeout(timeoutId); + timeoutId = setTimeout(function () { + if (cookies && cookies.length > 0) { + $.each(cookies, function (i, item) { + if (that.deleteCookie !== undefined) { + that.deleteCookie(item); + } + }); + } + }, that.options.searchTimeOut); // If there is not any value in the controls exit this method + + if (!hasValues) { + return; + } // Clear each type of filter if it exists. + // Requires the body to reload each time a type of filter is found because we never know + // which ones are going to be present. + + + if (controls.length > 0) { + this.filterColumnsPartial = {}; + $(controls[0]).trigger(controls[0].tagName === 'INPUT' ? 'keyup' : 'change', { + keyCode: 13 + }); + } else { + return; + } + + if (search.length > 0) { + that.resetSearch(); + } // use the default sort order if it exists. do nothing if it does not + + + if (that.options.sortName !== table.data('sortName') || that.options.sortOrder !== table.data('sortOrder')) { + var sorter = this.$header.find(Utils$1.sprintf('[data-field="%s"]', $(controls[0]).closest('table').data('sortName'))); + + if (sorter.length > 0) { + that.onSort({ + type: 'keypress', + currentTarget: sorter + }); + $(sorter).find('.sortable').trigger('click'); + } + } + } + } + }, { + key: "triggerSearch", + value: function triggerSearch() { + var searchControls = getSearchControls(this); + searchControls.each(function () { + var el = $(this); + + if (el.is('select')) { + el.change(); + } else { + el.keyup(); + } + }); + } + }, { + key: "enableControls", + value: function enableControls(enable) { + if (this.options.disableControlWhenSearch && this.options.sidePagination === 'server') { + var searchControls = getSearchControls(this); + + if (!enable) { + searchControls.prop('disabled', 'disabled'); + } else { + searchControls.removeProp('disabled'); + } + } + } + }, { + key: "toggleFilterControl", + value: function toggleFilterControl() { + this.options.filterControlVisible = !this.options.filterControlVisible; + var $filterControls = getControlContainer(this).find('.filter-control, .no-filter-control'); + + if (this.options.filterControlVisible) { + $filterControls.show(); + } else { + $filterControls.hide(); + this.clearFilterControl(); + } + + var icon = this.options.showButtonIcons ? this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow : ''; + var text = this.options.showButtonText ? this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow() : ''; + this.$toolbar.find('>.columns').find('.filter-control-switch').html(Utils$1.sprintf(this.constants.html.icon, this.options.iconsPrefix, icon) + ' ' + text); + } + }]); + + return _class; + }($.BootstrapTable); + +}))); diff --git a/InvenTree/InvenTree/static/script/bootstrap/filter-control-utils.js b/InvenTree/InvenTree/static/script/bootstrap/filter-control-utils.js new file mode 100644 index 0000000000..204efed32f --- /dev/null +++ b/InvenTree/InvenTree/static/script/bootstrap/filter-control-utils.js @@ -0,0 +1,2361 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery')) : + typeof define === 'function' && define.amd ? define(['exports', 'jquery'], factory) : + (global = global || self, factory(global.BootstrapTable = {}, global.jQuery)); +}(this, (function (exports, $) { 'use strict'; + + $ = $ && Object.prototype.hasOwnProperty.call($, 'default') ? $['default'] : $; + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var check = function (it) { + return it && it.Math == Math && it; + }; + + // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 + var global_1 = + // eslint-disable-next-line no-undef + check(typeof globalThis == 'object' && globalThis) || + check(typeof window == 'object' && window) || + check(typeof self == 'object' && self) || + check(typeof commonjsGlobal == 'object' && commonjsGlobal) || + // eslint-disable-next-line no-new-func + Function('return this')(); + + var fails = function (exec) { + try { + return !!exec(); + } catch (error) { + return true; + } + }; + + // Thank's IE8 for his funny defineProperty + var descriptors = !fails(function () { + return Object.defineProperty({}, 'a', { get: function () { return 7; } }).a != 7; + }); + + var nativePropertyIsEnumerable = {}.propertyIsEnumerable; + var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + + // Nashorn ~ JDK8 bug + var NASHORN_BUG = getOwnPropertyDescriptor && !nativePropertyIsEnumerable.call({ 1: 2 }, 1); + + // `Object.prototype.propertyIsEnumerable` method implementation + // https://tc39.github.io/ecma262/#sec-object.prototype.propertyisenumerable + var f = NASHORN_BUG ? function propertyIsEnumerable(V) { + var descriptor = getOwnPropertyDescriptor(this, V); + return !!descriptor && descriptor.enumerable; + } : nativePropertyIsEnumerable; + + var objectPropertyIsEnumerable = { + f: f + }; + + var createPropertyDescriptor = function (bitmap, value) { + return { + enumerable: !(bitmap & 1), + configurable: !(bitmap & 2), + writable: !(bitmap & 4), + value: value + }; + }; + + var toString = {}.toString; + + var classofRaw = function (it) { + return toString.call(it).slice(8, -1); + }; + + var split = ''.split; + + // fallback for non-array-like ES3 and non-enumerable old V8 strings + var indexedObject = fails(function () { + // throws an error in rhino, see https://github.com/mozilla/rhino/issues/346 + // eslint-disable-next-line no-prototype-builtins + return !Object('z').propertyIsEnumerable(0); + }) ? function (it) { + return classofRaw(it) == 'String' ? split.call(it, '') : Object(it); + } : Object; + + // `RequireObjectCoercible` abstract operation + // https://tc39.github.io/ecma262/#sec-requireobjectcoercible + var requireObjectCoercible = function (it) { + if (it == undefined) throw TypeError("Can't call method on " + it); + return it; + }; + + // toObject with fallback for non-array-like ES3 strings + + + + var toIndexedObject = function (it) { + return indexedObject(requireObjectCoercible(it)); + }; + + var isObject = function (it) { + return typeof it === 'object' ? it !== null : typeof it === 'function'; + }; + + // `ToPrimitive` abstract operation + // https://tc39.github.io/ecma262/#sec-toprimitive + // instead of the ES6 spec version, we didn't implement @@toPrimitive case + // and the second argument - flag - preferred type is a string + var toPrimitive = function (input, PREFERRED_STRING) { + if (!isObject(input)) return input; + var fn, val; + if (PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val; + if (typeof (fn = input.valueOf) == 'function' && !isObject(val = fn.call(input))) return val; + if (!PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val; + throw TypeError("Can't convert object to primitive value"); + }; + + var hasOwnProperty = {}.hasOwnProperty; + + var has = function (it, key) { + return hasOwnProperty.call(it, key); + }; + + var document$1 = global_1.document; + // typeof document.createElement is 'object' in old IE + var EXISTS = isObject(document$1) && isObject(document$1.createElement); + + var documentCreateElement = function (it) { + return EXISTS ? document$1.createElement(it) : {}; + }; + + // Thank's IE8 for his funny defineProperty + var ie8DomDefine = !descriptors && !fails(function () { + return Object.defineProperty(documentCreateElement('div'), 'a', { + get: function () { return 7; } + }).a != 7; + }); + + var nativeGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + + // `Object.getOwnPropertyDescriptor` method + // https://tc39.github.io/ecma262/#sec-object.getownpropertydescriptor + var f$1 = descriptors ? nativeGetOwnPropertyDescriptor : function getOwnPropertyDescriptor(O, P) { + O = toIndexedObject(O); + P = toPrimitive(P, true); + if (ie8DomDefine) try { + return nativeGetOwnPropertyDescriptor(O, P); + } catch (error) { /* empty */ } + if (has(O, P)) return createPropertyDescriptor(!objectPropertyIsEnumerable.f.call(O, P), O[P]); + }; + + var objectGetOwnPropertyDescriptor = { + f: f$1 + }; + + var anObject = function (it) { + if (!isObject(it)) { + throw TypeError(String(it) + ' is not an object'); + } return it; + }; + + var nativeDefineProperty = Object.defineProperty; + + // `Object.defineProperty` method + // https://tc39.github.io/ecma262/#sec-object.defineproperty + var f$2 = descriptors ? nativeDefineProperty : function defineProperty(O, P, Attributes) { + anObject(O); + P = toPrimitive(P, true); + anObject(Attributes); + if (ie8DomDefine) try { + return nativeDefineProperty(O, P, Attributes); + } catch (error) { /* empty */ } + if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported'); + if ('value' in Attributes) O[P] = Attributes.value; + return O; + }; + + var objectDefineProperty = { + f: f$2 + }; + + var createNonEnumerableProperty = descriptors ? function (object, key, value) { + return objectDefineProperty.f(object, key, createPropertyDescriptor(1, value)); + } : function (object, key, value) { + object[key] = value; + return object; + }; + + var setGlobal = function (key, value) { + try { + createNonEnumerableProperty(global_1, key, value); + } catch (error) { + global_1[key] = value; + } return value; + }; + + var SHARED = '__core-js_shared__'; + var store = global_1[SHARED] || setGlobal(SHARED, {}); + + var sharedStore = store; + + var functionToString = Function.toString; + + // this helper broken in `3.4.1-3.4.4`, so we can't use `shared` helper + if (typeof sharedStore.inspectSource != 'function') { + sharedStore.inspectSource = function (it) { + return functionToString.call(it); + }; + } + + var inspectSource = sharedStore.inspectSource; + + var WeakMap = global_1.WeakMap; + + var nativeWeakMap = typeof WeakMap === 'function' && /native code/.test(inspectSource(WeakMap)); + + var shared = createCommonjsModule(function (module) { + (module.exports = function (key, value) { + return sharedStore[key] || (sharedStore[key] = value !== undefined ? value : {}); + })('versions', []).push({ + version: '3.6.0', + mode: 'global', + copyright: '© 2019 Denis Pushkarev (zloirock.ru)' + }); + }); + + var id = 0; + var postfix = Math.random(); + + var uid = function (key) { + return 'Symbol(' + String(key === undefined ? '' : key) + ')_' + (++id + postfix).toString(36); + }; + + var keys = shared('keys'); + + var sharedKey = function (key) { + return keys[key] || (keys[key] = uid(key)); + }; + + var hiddenKeys = {}; + + var WeakMap$1 = global_1.WeakMap; + var set, get, has$1; + + var enforce = function (it) { + return has$1(it) ? get(it) : set(it, {}); + }; + + var getterFor = function (TYPE) { + return function (it) { + var state; + if (!isObject(it) || (state = get(it)).type !== TYPE) { + throw TypeError('Incompatible receiver, ' + TYPE + ' required'); + } return state; + }; + }; + + if (nativeWeakMap) { + var store$1 = new WeakMap$1(); + var wmget = store$1.get; + var wmhas = store$1.has; + var wmset = store$1.set; + set = function (it, metadata) { + wmset.call(store$1, it, metadata); + return metadata; + }; + get = function (it) { + return wmget.call(store$1, it) || {}; + }; + has$1 = function (it) { + return wmhas.call(store$1, it); + }; + } else { + var STATE = sharedKey('state'); + hiddenKeys[STATE] = true; + set = function (it, metadata) { + createNonEnumerableProperty(it, STATE, metadata); + return metadata; + }; + get = function (it) { + return has(it, STATE) ? it[STATE] : {}; + }; + has$1 = function (it) { + return has(it, STATE); + }; + } + + var internalState = { + set: set, + get: get, + has: has$1, + enforce: enforce, + getterFor: getterFor + }; + + var redefine = createCommonjsModule(function (module) { + var getInternalState = internalState.get; + var enforceInternalState = internalState.enforce; + var TEMPLATE = String(String).split('String'); + + (module.exports = function (O, key, value, options) { + var unsafe = options ? !!options.unsafe : false; + var simple = options ? !!options.enumerable : false; + var noTargetGet = options ? !!options.noTargetGet : false; + if (typeof value == 'function') { + if (typeof key == 'string' && !has(value, 'name')) createNonEnumerableProperty(value, 'name', key); + enforceInternalState(value).source = TEMPLATE.join(typeof key == 'string' ? key : ''); + } + if (O === global_1) { + if (simple) O[key] = value; + else setGlobal(key, value); + return; + } else if (!unsafe) { + delete O[key]; + } else if (!noTargetGet && O[key]) { + simple = true; + } + if (simple) O[key] = value; + else createNonEnumerableProperty(O, key, value); + // add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative + })(Function.prototype, 'toString', function toString() { + return typeof this == 'function' && getInternalState(this).source || inspectSource(this); + }); + }); + + var path = global_1; + + var aFunction = function (variable) { + return typeof variable == 'function' ? variable : undefined; + }; + + var getBuiltIn = function (namespace, method) { + return arguments.length < 2 ? aFunction(path[namespace]) || aFunction(global_1[namespace]) + : path[namespace] && path[namespace][method] || global_1[namespace] && global_1[namespace][method]; + }; + + var ceil = Math.ceil; + var floor = Math.floor; + + // `ToInteger` abstract operation + // https://tc39.github.io/ecma262/#sec-tointeger + var toInteger = function (argument) { + return isNaN(argument = +argument) ? 0 : (argument > 0 ? floor : ceil)(argument); + }; + + var min = Math.min; + + // `ToLength` abstract operation + // https://tc39.github.io/ecma262/#sec-tolength + var toLength = function (argument) { + return argument > 0 ? min(toInteger(argument), 0x1FFFFFFFFFFFFF) : 0; // 2 ** 53 - 1 == 9007199254740991 + }; + + var max = Math.max; + var min$1 = Math.min; + + // Helper for a popular repeating case of the spec: + // Let integer be ? ToInteger(index). + // If integer < 0, let result be max((length + integer), 0); else let result be min(integer, length). + var toAbsoluteIndex = function (index, length) { + var integer = toInteger(index); + return integer < 0 ? max(integer + length, 0) : min$1(integer, length); + }; + + // `Array.prototype.{ indexOf, includes }` methods implementation + var createMethod = function (IS_INCLUDES) { + return function ($this, el, fromIndex) { + var O = toIndexedObject($this); + var length = toLength(O.length); + var index = toAbsoluteIndex(fromIndex, length); + var value; + // Array#includes uses SameValueZero equality algorithm + // eslint-disable-next-line no-self-compare + if (IS_INCLUDES && el != el) while (length > index) { + value = O[index++]; + // eslint-disable-next-line no-self-compare + if (value != value) return true; + // Array#indexOf ignores holes, Array#includes - not + } else for (;length > index; index++) { + if ((IS_INCLUDES || index in O) && O[index] === el) return IS_INCLUDES || index || 0; + } return !IS_INCLUDES && -1; + }; + }; + + var arrayIncludes = { + // `Array.prototype.includes` method + // https://tc39.github.io/ecma262/#sec-array.prototype.includes + includes: createMethod(true), + // `Array.prototype.indexOf` method + // https://tc39.github.io/ecma262/#sec-array.prototype.indexof + indexOf: createMethod(false) + }; + + var indexOf = arrayIncludes.indexOf; + + + var objectKeysInternal = function (object, names) { + var O = toIndexedObject(object); + var i = 0; + var result = []; + var key; + for (key in O) !has(hiddenKeys, key) && has(O, key) && result.push(key); + // Don't enum bug & hidden keys + while (names.length > i) if (has(O, key = names[i++])) { + ~indexOf(result, key) || result.push(key); + } + return result; + }; + + // IE8- don't enum bug keys + var enumBugKeys = [ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toLocaleString', + 'toString', + 'valueOf' + ]; + + var hiddenKeys$1 = enumBugKeys.concat('length', 'prototype'); + + // `Object.getOwnPropertyNames` method + // https://tc39.github.io/ecma262/#sec-object.getownpropertynames + var f$3 = Object.getOwnPropertyNames || function getOwnPropertyNames(O) { + return objectKeysInternal(O, hiddenKeys$1); + }; + + var objectGetOwnPropertyNames = { + f: f$3 + }; + + var f$4 = Object.getOwnPropertySymbols; + + var objectGetOwnPropertySymbols = { + f: f$4 + }; + + // all object keys, includes non-enumerable and symbols + var ownKeys = getBuiltIn('Reflect', 'ownKeys') || function ownKeys(it) { + var keys = objectGetOwnPropertyNames.f(anObject(it)); + var getOwnPropertySymbols = objectGetOwnPropertySymbols.f; + return getOwnPropertySymbols ? keys.concat(getOwnPropertySymbols(it)) : keys; + }; + + var copyConstructorProperties = function (target, source) { + var keys = ownKeys(source); + var defineProperty = objectDefineProperty.f; + var getOwnPropertyDescriptor = objectGetOwnPropertyDescriptor.f; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (!has(target, key)) defineProperty(target, key, getOwnPropertyDescriptor(source, key)); + } + }; + + var replacement = /#|\.prototype\./; + + var isForced = function (feature, detection) { + var value = data[normalize(feature)]; + return value == POLYFILL ? true + : value == NATIVE ? false + : typeof detection == 'function' ? fails(detection) + : !!detection; + }; + + var normalize = isForced.normalize = function (string) { + return String(string).replace(replacement, '.').toLowerCase(); + }; + + var data = isForced.data = {}; + var NATIVE = isForced.NATIVE = 'N'; + var POLYFILL = isForced.POLYFILL = 'P'; + + var isForced_1 = isForced; + + var getOwnPropertyDescriptor$1 = objectGetOwnPropertyDescriptor.f; + + + + + + + /* + options.target - name of the target object + options.global - target is the global object + options.stat - export as static methods of target + options.proto - export as prototype methods of target + options.real - real prototype method for the `pure` version + options.forced - export even if the native feature is available + options.bind - bind methods to the target, required for the `pure` version + options.wrap - wrap constructors to preventing global pollution, required for the `pure` version + options.unsafe - use the simple assignment of property instead of delete + defineProperty + options.sham - add a flag to not completely full polyfills + options.enumerable - export as enumerable property + options.noTargetGet - prevent calling a getter on target + */ + var _export = function (options, source) { + var TARGET = options.target; + var GLOBAL = options.global; + var STATIC = options.stat; + var FORCED, target, key, targetProperty, sourceProperty, descriptor; + if (GLOBAL) { + target = global_1; + } else if (STATIC) { + target = global_1[TARGET] || setGlobal(TARGET, {}); + } else { + target = (global_1[TARGET] || {}).prototype; + } + if (target) for (key in source) { + sourceProperty = source[key]; + if (options.noTargetGet) { + descriptor = getOwnPropertyDescriptor$1(target, key); + targetProperty = descriptor && descriptor.value; + } else targetProperty = target[key]; + FORCED = isForced_1(GLOBAL ? key : TARGET + (STATIC ? '.' : '#') + key, options.forced); + // contained in target + if (!FORCED && targetProperty !== undefined) { + if (typeof sourceProperty === typeof targetProperty) continue; + copyConstructorProperties(sourceProperty, targetProperty); + } + // add a flag to not completely full polyfills + if (options.sham || (targetProperty && targetProperty.sham)) { + createNonEnumerableProperty(sourceProperty, 'sham', true); + } + // extend global + redefine(target, key, sourceProperty, options); + } + }; + + // `IsArray` abstract operation + // https://tc39.github.io/ecma262/#sec-isarray + var isArray = Array.isArray || function isArray(arg) { + return classofRaw(arg) == 'Array'; + }; + + // `ToObject` abstract operation + // https://tc39.github.io/ecma262/#sec-toobject + var toObject = function (argument) { + return Object(requireObjectCoercible(argument)); + }; + + var createProperty = function (object, key, value) { + var propertyKey = toPrimitive(key); + if (propertyKey in object) objectDefineProperty.f(object, propertyKey, createPropertyDescriptor(0, value)); + else object[propertyKey] = value; + }; + + var nativeSymbol = !!Object.getOwnPropertySymbols && !fails(function () { + // Chrome 38 Symbol has incorrect toString conversion + // eslint-disable-next-line no-undef + return !String(Symbol()); + }); + + var useSymbolAsUid = nativeSymbol + // eslint-disable-next-line no-undef + && !Symbol.sham + // eslint-disable-next-line no-undef + && typeof Symbol() == 'symbol'; + + var WellKnownSymbolsStore = shared('wks'); + var Symbol$1 = global_1.Symbol; + var createWellKnownSymbol = useSymbolAsUid ? Symbol$1 : uid; + + var wellKnownSymbol = function (name) { + if (!has(WellKnownSymbolsStore, name)) { + if (nativeSymbol && has(Symbol$1, name)) WellKnownSymbolsStore[name] = Symbol$1[name]; + else WellKnownSymbolsStore[name] = createWellKnownSymbol('Symbol.' + name); + } return WellKnownSymbolsStore[name]; + }; + + var SPECIES = wellKnownSymbol('species'); + + // `ArraySpeciesCreate` abstract operation + // https://tc39.github.io/ecma262/#sec-arrayspeciescreate + var arraySpeciesCreate = function (originalArray, length) { + var C; + if (isArray(originalArray)) { + C = originalArray.constructor; + // cross-realm fallback + if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined; + else if (isObject(C)) { + C = C[SPECIES]; + if (C === null) C = undefined; + } + } return new (C === undefined ? Array : C)(length === 0 ? 0 : length); + }; + + var userAgent = getBuiltIn('navigator', 'userAgent') || ''; + + var process = global_1.process; + var versions = process && process.versions; + var v8 = versions && versions.v8; + var match, version; + + if (v8) { + match = v8.split('.'); + version = match[0] + match[1]; + } else if (userAgent) { + match = userAgent.match(/Edge\/(\d+)/); + if (!match || match[1] >= 74) { + match = userAgent.match(/Chrome\/(\d+)/); + if (match) version = match[1]; + } + } + + var v8Version = version && +version; + + var SPECIES$1 = wellKnownSymbol('species'); + + var arrayMethodHasSpeciesSupport = function (METHOD_NAME) { + // We can't use this feature detection in V8 since it causes + // deoptimization and serious performance degradation + // https://github.com/zloirock/core-js/issues/677 + return v8Version >= 51 || !fails(function () { + var array = []; + var constructor = array.constructor = {}; + constructor[SPECIES$1] = function () { + return { foo: 1 }; + }; + return array[METHOD_NAME](Boolean).foo !== 1; + }); + }; + + var IS_CONCAT_SPREADABLE = wellKnownSymbol('isConcatSpreadable'); + var MAX_SAFE_INTEGER = 0x1FFFFFFFFFFFFF; + var MAXIMUM_ALLOWED_INDEX_EXCEEDED = 'Maximum allowed index exceeded'; + + // We can't use this feature detection in V8 since it causes + // deoptimization and serious performance degradation + // https://github.com/zloirock/core-js/issues/679 + var IS_CONCAT_SPREADABLE_SUPPORT = v8Version >= 51 || !fails(function () { + var array = []; + array[IS_CONCAT_SPREADABLE] = false; + return array.concat()[0] !== array; + }); + + var SPECIES_SUPPORT = arrayMethodHasSpeciesSupport('concat'); + + var isConcatSpreadable = function (O) { + if (!isObject(O)) return false; + var spreadable = O[IS_CONCAT_SPREADABLE]; + return spreadable !== undefined ? !!spreadable : isArray(O); + }; + + var FORCED = !IS_CONCAT_SPREADABLE_SUPPORT || !SPECIES_SUPPORT; + + // `Array.prototype.concat` method + // https://tc39.github.io/ecma262/#sec-array.prototype.concat + // with adding support of @@isConcatSpreadable and @@species + _export({ target: 'Array', proto: true, forced: FORCED }, { + concat: function concat(arg) { // eslint-disable-line no-unused-vars + var O = toObject(this); + var A = arraySpeciesCreate(O, 0); + var n = 0; + var i, k, length, len, E; + for (i = -1, length = arguments.length; i < length; i++) { + E = i === -1 ? O : arguments[i]; + if (isConcatSpreadable(E)) { + len = toLength(E.length); + if (n + len > MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED); + for (k = 0; k < len; k++, n++) if (k in E) createProperty(A, n, E[k]); + } else { + if (n >= MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED); + createProperty(A, n++, E); + } + } + A.length = n; + return A; + } + }); + + var aFunction$1 = function (it) { + if (typeof it != 'function') { + throw TypeError(String(it) + ' is not a function'); + } return it; + }; + + // optional / simple context binding + var bindContext = function (fn, that, length) { + aFunction$1(fn); + if (that === undefined) return fn; + switch (length) { + case 0: return function () { + return fn.call(that); + }; + case 1: return function (a) { + return fn.call(that, a); + }; + case 2: return function (a, b) { + return fn.call(that, a, b); + }; + case 3: return function (a, b, c) { + return fn.call(that, a, b, c); + }; + } + return function (/* ...args */) { + return fn.apply(that, arguments); + }; + }; + + var push = [].push; + + // `Array.prototype.{ forEach, map, filter, some, every, find, findIndex }` methods implementation + var createMethod$1 = function (TYPE) { + var IS_MAP = TYPE == 1; + var IS_FILTER = TYPE == 2; + var IS_SOME = TYPE == 3; + var IS_EVERY = TYPE == 4; + var IS_FIND_INDEX = TYPE == 6; + var NO_HOLES = TYPE == 5 || IS_FIND_INDEX; + return function ($this, callbackfn, that, specificCreate) { + var O = toObject($this); + var self = indexedObject(O); + var boundFunction = bindContext(callbackfn, that, 3); + var length = toLength(self.length); + var index = 0; + var create = specificCreate || arraySpeciesCreate; + var target = IS_MAP ? create($this, length) : IS_FILTER ? create($this, 0) : undefined; + var value, result; + for (;length > index; index++) if (NO_HOLES || index in self) { + value = self[index]; + result = boundFunction(value, index, O); + if (TYPE) { + if (IS_MAP) target[index] = result; // map + else if (result) switch (TYPE) { + case 3: return true; // some + case 5: return value; // find + case 6: return index; // findIndex + case 2: push.call(target, value); // filter + } else if (IS_EVERY) return false; // every + } + } + return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : target; + }; + }; + + var arrayIteration = { + // `Array.prototype.forEach` method + // https://tc39.github.io/ecma262/#sec-array.prototype.foreach + forEach: createMethod$1(0), + // `Array.prototype.map` method + // https://tc39.github.io/ecma262/#sec-array.prototype.map + map: createMethod$1(1), + // `Array.prototype.filter` method + // https://tc39.github.io/ecma262/#sec-array.prototype.filter + filter: createMethod$1(2), + // `Array.prototype.some` method + // https://tc39.github.io/ecma262/#sec-array.prototype.some + some: createMethod$1(3), + // `Array.prototype.every` method + // https://tc39.github.io/ecma262/#sec-array.prototype.every + every: createMethod$1(4), + // `Array.prototype.find` method + // https://tc39.github.io/ecma262/#sec-array.prototype.find + find: createMethod$1(5), + // `Array.prototype.findIndex` method + // https://tc39.github.io/ecma262/#sec-array.prototype.findIndex + findIndex: createMethod$1(6) + }; + + var $filter = arrayIteration.filter; + + + + var HAS_SPECIES_SUPPORT = arrayMethodHasSpeciesSupport('filter'); + // Edge 14- issue + var USES_TO_LENGTH = HAS_SPECIES_SUPPORT && !fails(function () { + [].filter.call({ length: -1, 0: 1 }, function (it) { throw it; }); + }); + + // `Array.prototype.filter` method + // https://tc39.github.io/ecma262/#sec-array.prototype.filter + // with adding support of @@species + _export({ target: 'Array', proto: true, forced: !HAS_SPECIES_SUPPORT || !USES_TO_LENGTH }, { + filter: function filter(callbackfn /* , thisArg */) { + return $filter(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + // `Object.keys` method + // https://tc39.github.io/ecma262/#sec-object.keys + var objectKeys = Object.keys || function keys(O) { + return objectKeysInternal(O, enumBugKeys); + }; + + // `Object.defineProperties` method + // https://tc39.github.io/ecma262/#sec-object.defineproperties + var objectDefineProperties = descriptors ? Object.defineProperties : function defineProperties(O, Properties) { + anObject(O); + var keys = objectKeys(Properties); + var length = keys.length; + var index = 0; + var key; + while (length > index) objectDefineProperty.f(O, key = keys[index++], Properties[key]); + return O; + }; + + var html = getBuiltIn('document', 'documentElement'); + + var GT = '>'; + var LT = '<'; + var PROTOTYPE = 'prototype'; + var SCRIPT = 'script'; + var IE_PROTO = sharedKey('IE_PROTO'); + + var EmptyConstructor = function () { /* empty */ }; + + var scriptTag = function (content) { + return LT + SCRIPT + GT + content + LT + '/' + SCRIPT + GT; + }; + + // Create object with fake `null` prototype: use ActiveX Object with cleared prototype + var NullProtoObjectViaActiveX = function (activeXDocument) { + activeXDocument.write(scriptTag('')); + activeXDocument.close(); + var temp = activeXDocument.parentWindow.Object; + activeXDocument = null; // avoid memory leak + return temp; + }; + + // Create object with fake `null` prototype: use iframe Object with cleared prototype + var NullProtoObjectViaIFrame = function () { + // Thrash, waste and sodomy: IE GC bug + var iframe = documentCreateElement('iframe'); + var JS = 'java' + SCRIPT + ':'; + var iframeDocument; + iframe.style.display = 'none'; + html.appendChild(iframe); + // https://github.com/zloirock/core-js/issues/475 + iframe.src = String(JS); + iframeDocument = iframe.contentWindow.document; + iframeDocument.open(); + iframeDocument.write(scriptTag('document.F=Object')); + iframeDocument.close(); + return iframeDocument.F; + }; + + // Check for document.domain and active x support + // No need to use active x approach when document.domain is not set + // see https://github.com/es-shims/es5-shim/issues/150 + // variation of https://github.com/kitcambridge/es5-shim/commit/4f738ac066346 + // avoid IE GC bug + var activeXDocument; + var NullProtoObject = function () { + try { + /* global ActiveXObject */ + activeXDocument = document.domain && new ActiveXObject('htmlfile'); + } catch (error) { /* ignore */ } + NullProtoObject = activeXDocument ? NullProtoObjectViaActiveX(activeXDocument) : NullProtoObjectViaIFrame(); + var length = enumBugKeys.length; + while (length--) delete NullProtoObject[PROTOTYPE][enumBugKeys[length]]; + return NullProtoObject(); + }; + + hiddenKeys[IE_PROTO] = true; + + // `Object.create` method + // https://tc39.github.io/ecma262/#sec-object.create + var objectCreate = Object.create || function create(O, Properties) { + var result; + if (O !== null) { + EmptyConstructor[PROTOTYPE] = anObject(O); + result = new EmptyConstructor(); + EmptyConstructor[PROTOTYPE] = null; + // add "__proto__" for Object.getPrototypeOf polyfill + result[IE_PROTO] = O; + } else result = NullProtoObject(); + return Properties === undefined ? result : objectDefineProperties(result, Properties); + }; + + var UNSCOPABLES = wellKnownSymbol('unscopables'); + var ArrayPrototype = Array.prototype; + + // Array.prototype[@@unscopables] + // https://tc39.github.io/ecma262/#sec-array.prototype-@@unscopables + if (ArrayPrototype[UNSCOPABLES] == undefined) { + objectDefineProperty.f(ArrayPrototype, UNSCOPABLES, { + configurable: true, + value: objectCreate(null) + }); + } + + // add a key to Array.prototype[@@unscopables] + var addToUnscopables = function (key) { + ArrayPrototype[UNSCOPABLES][key] = true; + }; + + var $find = arrayIteration.find; + + + var FIND = 'find'; + var SKIPS_HOLES = true; + + // Shouldn't skip holes + if (FIND in []) Array(1)[FIND](function () { SKIPS_HOLES = false; }); + + // `Array.prototype.find` method + // https://tc39.github.io/ecma262/#sec-array.prototype.find + _export({ target: 'Array', proto: true, forced: SKIPS_HOLES }, { + find: function find(callbackfn /* , that = undefined */) { + return $find(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + // https://tc39.github.io/ecma262/#sec-array.prototype-@@unscopables + addToUnscopables(FIND); + + var $includes = arrayIncludes.includes; + + + // `Array.prototype.includes` method + // https://tc39.github.io/ecma262/#sec-array.prototype.includes + _export({ target: 'Array', proto: true }, { + includes: function includes(el /* , fromIndex = 0 */) { + return $includes(this, el, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + // https://tc39.github.io/ecma262/#sec-array.prototype-@@unscopables + addToUnscopables('includes'); + + var sloppyArrayMethod = function (METHOD_NAME, argument) { + var method = [][METHOD_NAME]; + return !method || !fails(function () { + // eslint-disable-next-line no-useless-call,no-throw-literal + method.call(null, argument || function () { throw 1; }, 1); + }); + }; + + var $indexOf = arrayIncludes.indexOf; + + + var nativeIndexOf = [].indexOf; + + var NEGATIVE_ZERO = !!nativeIndexOf && 1 / [1].indexOf(1, -0) < 0; + var SLOPPY_METHOD = sloppyArrayMethod('indexOf'); + + // `Array.prototype.indexOf` method + // https://tc39.github.io/ecma262/#sec-array.prototype.indexof + _export({ target: 'Array', proto: true, forced: NEGATIVE_ZERO || SLOPPY_METHOD }, { + indexOf: function indexOf(searchElement /* , fromIndex = 0 */) { + return NEGATIVE_ZERO + // convert -0 to +0 + ? nativeIndexOf.apply(this, arguments) || 0 + : $indexOf(this, searchElement, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + var nativeJoin = [].join; + + var ES3_STRINGS = indexedObject != Object; + var SLOPPY_METHOD$1 = sloppyArrayMethod('join', ','); + + // `Array.prototype.join` method + // https://tc39.github.io/ecma262/#sec-array.prototype.join + _export({ target: 'Array', proto: true, forced: ES3_STRINGS || SLOPPY_METHOD$1 }, { + join: function join(separator) { + return nativeJoin.call(toIndexedObject(this), separator === undefined ? ',' : separator); + } + }); + + var test = []; + var nativeSort = test.sort; + + // IE8- + var FAILS_ON_UNDEFINED = fails(function () { + test.sort(undefined); + }); + // V8 bug + var FAILS_ON_NULL = fails(function () { + test.sort(null); + }); + // Old WebKit + var SLOPPY_METHOD$2 = sloppyArrayMethod('sort'); + + var FORCED$1 = FAILS_ON_UNDEFINED || !FAILS_ON_NULL || SLOPPY_METHOD$2; + + // `Array.prototype.sort` method + // https://tc39.github.io/ecma262/#sec-array.prototype.sort + _export({ target: 'Array', proto: true, forced: FORCED$1 }, { + sort: function sort(comparefn) { + return comparefn === undefined + ? nativeSort.call(toObject(this)) + : nativeSort.call(toObject(this), aFunction$1(comparefn)); + } + }); + + var FAILS_ON_PRIMITIVES = fails(function () { objectKeys(1); }); + + // `Object.keys` method + // https://tc39.github.io/ecma262/#sec-object.keys + _export({ target: 'Object', stat: true, forced: FAILS_ON_PRIMITIVES }, { + keys: function keys(it) { + return objectKeys(toObject(it)); + } + }); + + var TO_STRING_TAG = wellKnownSymbol('toStringTag'); + var test$1 = {}; + + test$1[TO_STRING_TAG] = 'z'; + + var toStringTagSupport = String(test$1) === '[object z]'; + + var TO_STRING_TAG$1 = wellKnownSymbol('toStringTag'); + // ES3 wrong here + var CORRECT_ARGUMENTS = classofRaw(function () { return arguments; }()) == 'Arguments'; + + // fallback for IE11 Script Access Denied error + var tryGet = function (it, key) { + try { + return it[key]; + } catch (error) { /* empty */ } + }; + + // getting tag from ES6+ `Object.prototype.toString` + var classof = toStringTagSupport ? classofRaw : function (it) { + var O, tag, result; + return it === undefined ? 'Undefined' : it === null ? 'Null' + // @@toStringTag case + : typeof (tag = tryGet(O = Object(it), TO_STRING_TAG$1)) == 'string' ? tag + // builtinTag case + : CORRECT_ARGUMENTS ? classofRaw(O) + // ES3 arguments fallback + : (result = classofRaw(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : result; + }; + + // `Object.prototype.toString` method implementation + // https://tc39.github.io/ecma262/#sec-object.prototype.tostring + var objectToString = toStringTagSupport ? {}.toString : function toString() { + return '[object ' + classof(this) + ']'; + }; + + // `Object.prototype.toString` method + // https://tc39.github.io/ecma262/#sec-object.prototype.tostring + if (!toStringTagSupport) { + redefine(Object.prototype, 'toString', objectToString, { unsafe: true }); + } + + // `RegExp.prototype.flags` getter implementation + // https://tc39.github.io/ecma262/#sec-get-regexp.prototype.flags + var regexpFlags = function () { + var that = anObject(this); + var result = ''; + if (that.global) result += 'g'; + if (that.ignoreCase) result += 'i'; + if (that.multiline) result += 'm'; + if (that.dotAll) result += 's'; + if (that.unicode) result += 'u'; + if (that.sticky) result += 'y'; + return result; + }; + + // babel-minify transpiles RegExp('a', 'y') -> /a/y and it causes SyntaxError, + // so we use an intermediate function. + function RE(s, f) { + return RegExp(s, f); + } + + var UNSUPPORTED_Y = fails(function () { + // babel-minify transpiles RegExp('a', 'y') -> /a/y and it causes SyntaxError + var re = RE('a', 'y'); + re.lastIndex = 2; + return re.exec('abcd') != null; + }); + + var BROKEN_CARET = fails(function () { + // https://bugzilla.mozilla.org/show_bug.cgi?id=773687 + var re = RE('^r', 'gy'); + re.lastIndex = 2; + return re.exec('str') != null; + }); + + var regexpStickyHelpers = { + UNSUPPORTED_Y: UNSUPPORTED_Y, + BROKEN_CARET: BROKEN_CARET + }; + + var nativeExec = RegExp.prototype.exec; + // This always refers to the native implementation, because the + // String#replace polyfill uses ./fix-regexp-well-known-symbol-logic.js, + // which loads this file before patching the method. + var nativeReplace = String.prototype.replace; + + var patchedExec = nativeExec; + + var UPDATES_LAST_INDEX_WRONG = (function () { + var re1 = /a/; + var re2 = /b*/g; + nativeExec.call(re1, 'a'); + nativeExec.call(re2, 'a'); + return re1.lastIndex !== 0 || re2.lastIndex !== 0; + })(); + + var UNSUPPORTED_Y$1 = regexpStickyHelpers.UNSUPPORTED_Y || regexpStickyHelpers.BROKEN_CARET; + + // nonparticipating capturing group, copied from es5-shim's String#split patch. + var NPCG_INCLUDED = /()??/.exec('')[1] !== undefined; + + var PATCH = UPDATES_LAST_INDEX_WRONG || NPCG_INCLUDED || UNSUPPORTED_Y$1; + + if (PATCH) { + patchedExec = function exec(str) { + var re = this; + var lastIndex, reCopy, match, i; + var sticky = UNSUPPORTED_Y$1 && re.sticky; + var flags = regexpFlags.call(re); + var source = re.source; + var charsAdded = 0; + var strCopy = str; + + if (sticky) { + flags = flags.replace('y', ''); + if (flags.indexOf('g') === -1) { + flags += 'g'; + } + + strCopy = String(str).slice(re.lastIndex); + // Support anchored sticky behavior. + if (re.lastIndex > 0 && (!re.multiline || re.multiline && str[re.lastIndex - 1] !== '\n')) { + source = '(?: ' + source + ')'; + strCopy = ' ' + strCopy; + charsAdded++; + } + // ^(? + rx + ) is needed, in combination with some str slicing, to + // simulate the 'y' flag. + reCopy = new RegExp('^(?:' + source + ')', flags); + } + + if (NPCG_INCLUDED) { + reCopy = new RegExp('^' + source + '$(?!\\s)', flags); + } + if (UPDATES_LAST_INDEX_WRONG) lastIndex = re.lastIndex; + + match = nativeExec.call(sticky ? reCopy : re, strCopy); + + if (sticky) { + if (match) { + match.input = match.input.slice(charsAdded); + match[0] = match[0].slice(charsAdded); + match.index = re.lastIndex; + re.lastIndex += match[0].length; + } else re.lastIndex = 0; + } else if (UPDATES_LAST_INDEX_WRONG && match) { + re.lastIndex = re.global ? match.index + match[0].length : lastIndex; + } + if (NPCG_INCLUDED && match && match.length > 1) { + // Fix browsers whose `exec` methods don't consistently return `undefined` + // for NPCG, like IE8. NOTE: This doesn' work for /(.?)?/ + nativeReplace.call(match[0], reCopy, function () { + for (i = 1; i < arguments.length - 2; i++) { + if (arguments[i] === undefined) match[i] = undefined; + } + }); + } + + return match; + }; + } + + var regexpExec = patchedExec; + + _export({ target: 'RegExp', proto: true, forced: /./.exec !== regexpExec }, { + exec: regexpExec + }); + + var TO_STRING = 'toString'; + var RegExpPrototype = RegExp.prototype; + var nativeToString = RegExpPrototype[TO_STRING]; + + var NOT_GENERIC = fails(function () { return nativeToString.call({ source: 'a', flags: 'b' }) != '/a/b'; }); + // FF44- RegExp#toString has a wrong name + var INCORRECT_NAME = nativeToString.name != TO_STRING; + + // `RegExp.prototype.toString` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype.tostring + if (NOT_GENERIC || INCORRECT_NAME) { + redefine(RegExp.prototype, TO_STRING, function toString() { + var R = anObject(this); + var p = String(R.source); + var rf = R.flags; + var f = String(rf === undefined && R instanceof RegExp && !('flags' in RegExpPrototype) ? regexpFlags.call(R) : rf); + return '/' + p + '/' + f; + }, { unsafe: true }); + } + + var SPECIES$2 = wellKnownSymbol('species'); + + var REPLACE_SUPPORTS_NAMED_GROUPS = !fails(function () { + // #replace needs built-in support for named groups. + // #match works fine because it just return the exec results, even if it has + // a "grops" property. + var re = /./; + re.exec = function () { + var result = []; + result.groups = { a: '7' }; + return result; + }; + return ''.replace(re, '$<a>') !== '7'; + }); + + // IE <= 11 replaces $0 with the whole match, as if it was $& + // https://stackoverflow.com/questions/6024666/getting-ie-to-replace-a-regex-with-the-literal-string-0 + var REPLACE_KEEPS_$0 = (function () { + return 'a'.replace(/./, '$0') === '$0'; + })(); + + // Chrome 51 has a buggy "split" implementation when RegExp#exec !== nativeExec + // Weex JS has frozen built-in prototypes, so use try / catch wrapper + var SPLIT_WORKS_WITH_OVERWRITTEN_EXEC = !fails(function () { + var re = /(?:)/; + var originalExec = re.exec; + re.exec = function () { return originalExec.apply(this, arguments); }; + var result = 'ab'.split(re); + return result.length !== 2 || result[0] !== 'a' || result[1] !== 'b'; + }); + + var fixRegexpWellKnownSymbolLogic = function (KEY, length, exec, sham) { + var SYMBOL = wellKnownSymbol(KEY); + + var DELEGATES_TO_SYMBOL = !fails(function () { + // String methods call symbol-named RegEp methods + var O = {}; + O[SYMBOL] = function () { return 7; }; + return ''[KEY](O) != 7; + }); + + var DELEGATES_TO_EXEC = DELEGATES_TO_SYMBOL && !fails(function () { + // Symbol-named RegExp methods call .exec + var execCalled = false; + var re = /a/; + + if (KEY === 'split') { + // We can't use real regex here since it causes deoptimization + // and serious performance degradation in V8 + // https://github.com/zloirock/core-js/issues/306 + re = {}; + // RegExp[@@split] doesn't call the regex's exec method, but first creates + // a new one. We need to return the patched regex when creating the new one. + re.constructor = {}; + re.constructor[SPECIES$2] = function () { return re; }; + re.flags = ''; + re[SYMBOL] = /./[SYMBOL]; + } + + re.exec = function () { execCalled = true; return null; }; + + re[SYMBOL](''); + return !execCalled; + }); + + if ( + !DELEGATES_TO_SYMBOL || + !DELEGATES_TO_EXEC || + (KEY === 'replace' && !(REPLACE_SUPPORTS_NAMED_GROUPS && REPLACE_KEEPS_$0)) || + (KEY === 'split' && !SPLIT_WORKS_WITH_OVERWRITTEN_EXEC) + ) { + var nativeRegExpMethod = /./[SYMBOL]; + var methods = exec(SYMBOL, ''[KEY], function (nativeMethod, regexp, str, arg2, forceStringMethod) { + if (regexp.exec === regexpExec) { + if (DELEGATES_TO_SYMBOL && !forceStringMethod) { + // The native String method already delegates to @@method (this + // polyfilled function), leasing to infinite recursion. + // We avoid it by directly calling the native @@method method. + return { done: true, value: nativeRegExpMethod.call(regexp, str, arg2) }; + } + return { done: true, value: nativeMethod.call(str, regexp, arg2) }; + } + return { done: false }; + }, { REPLACE_KEEPS_$0: REPLACE_KEEPS_$0 }); + var stringMethod = methods[0]; + var regexMethod = methods[1]; + + redefine(String.prototype, KEY, stringMethod); + redefine(RegExp.prototype, SYMBOL, length == 2 + // 21.2.5.8 RegExp.prototype[@@replace](string, replaceValue) + // 21.2.5.11 RegExp.prototype[@@split](string, limit) + ? function (string, arg) { return regexMethod.call(string, this, arg); } + // 21.2.5.6 RegExp.prototype[@@match](string) + // 21.2.5.9 RegExp.prototype[@@search](string) + : function (string) { return regexMethod.call(string, this); } + ); + } + + if (sham) createNonEnumerableProperty(RegExp.prototype[SYMBOL], 'sham', true); + }; + + // `String.prototype.{ codePointAt, at }` methods implementation + var createMethod$2 = function (CONVERT_TO_STRING) { + return function ($this, pos) { + var S = String(requireObjectCoercible($this)); + var position = toInteger(pos); + var size = S.length; + var first, second; + if (position < 0 || position >= size) return CONVERT_TO_STRING ? '' : undefined; + first = S.charCodeAt(position); + return first < 0xD800 || first > 0xDBFF || position + 1 === size + || (second = S.charCodeAt(position + 1)) < 0xDC00 || second > 0xDFFF + ? CONVERT_TO_STRING ? S.charAt(position) : first + : CONVERT_TO_STRING ? S.slice(position, position + 2) : (first - 0xD800 << 10) + (second - 0xDC00) + 0x10000; + }; + }; + + var stringMultibyte = { + // `String.prototype.codePointAt` method + // https://tc39.github.io/ecma262/#sec-string.prototype.codepointat + codeAt: createMethod$2(false), + // `String.prototype.at` method + // https://github.com/mathiasbynens/String.prototype.at + charAt: createMethod$2(true) + }; + + var charAt = stringMultibyte.charAt; + + // `AdvanceStringIndex` abstract operation + // https://tc39.github.io/ecma262/#sec-advancestringindex + var advanceStringIndex = function (S, index, unicode) { + return index + (unicode ? charAt(S, index).length : 1); + }; + + // `RegExpExec` abstract operation + // https://tc39.github.io/ecma262/#sec-regexpexec + var regexpExecAbstract = function (R, S) { + var exec = R.exec; + if (typeof exec === 'function') { + var result = exec.call(R, S); + if (typeof result !== 'object') { + throw TypeError('RegExp exec method returned something other than an Object or null'); + } + return result; + } + + if (classofRaw(R) !== 'RegExp') { + throw TypeError('RegExp#exec called on incompatible receiver'); + } + + return regexpExec.call(R, S); + }; + + // @@match logic + fixRegexpWellKnownSymbolLogic('match', 1, function (MATCH, nativeMatch, maybeCallNative) { + return [ + // `String.prototype.match` method + // https://tc39.github.io/ecma262/#sec-string.prototype.match + function match(regexp) { + var O = requireObjectCoercible(this); + var matcher = regexp == undefined ? undefined : regexp[MATCH]; + return matcher !== undefined ? matcher.call(regexp, O) : new RegExp(regexp)[MATCH](String(O)); + }, + // `RegExp.prototype[@@match]` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@match + function (regexp) { + var res = maybeCallNative(nativeMatch, regexp, this); + if (res.done) return res.value; + + var rx = anObject(regexp); + var S = String(this); + + if (!rx.global) return regexpExecAbstract(rx, S); + + var fullUnicode = rx.unicode; + rx.lastIndex = 0; + var A = []; + var n = 0; + var result; + while ((result = regexpExecAbstract(rx, S)) !== null) { + var matchStr = String(result[0]); + A[n] = matchStr; + if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode); + n++; + } + return n === 0 ? null : A; + } + ]; + }); + + var max$1 = Math.max; + var min$2 = Math.min; + var floor$1 = Math.floor; + var SUBSTITUTION_SYMBOLS = /\$([$&'`]|\d\d?|<[^>]*>)/g; + var SUBSTITUTION_SYMBOLS_NO_NAMED = /\$([$&'`]|\d\d?)/g; + + var maybeToString = function (it) { + return it === undefined ? it : String(it); + }; + + // @@replace logic + fixRegexpWellKnownSymbolLogic('replace', 2, function (REPLACE, nativeReplace, maybeCallNative, reason) { + return [ + // `String.prototype.replace` method + // https://tc39.github.io/ecma262/#sec-string.prototype.replace + function replace(searchValue, replaceValue) { + var O = requireObjectCoercible(this); + var replacer = searchValue == undefined ? undefined : searchValue[REPLACE]; + return replacer !== undefined + ? replacer.call(searchValue, O, replaceValue) + : nativeReplace.call(String(O), searchValue, replaceValue); + }, + // `RegExp.prototype[@@replace]` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@replace + function (regexp, replaceValue) { + if (reason.REPLACE_KEEPS_$0 || (typeof replaceValue === 'string' && replaceValue.indexOf('$0') === -1)) { + var res = maybeCallNative(nativeReplace, regexp, this, replaceValue); + if (res.done) return res.value; + } + + var rx = anObject(regexp); + var S = String(this); + + var functionalReplace = typeof replaceValue === 'function'; + if (!functionalReplace) replaceValue = String(replaceValue); + + var global = rx.global; + if (global) { + var fullUnicode = rx.unicode; + rx.lastIndex = 0; + } + var results = []; + while (true) { + var result = regexpExecAbstract(rx, S); + if (result === null) break; + + results.push(result); + if (!global) break; + + var matchStr = String(result[0]); + if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode); + } + + var accumulatedResult = ''; + var nextSourcePosition = 0; + for (var i = 0; i < results.length; i++) { + result = results[i]; + + var matched = String(result[0]); + var position = max$1(min$2(toInteger(result.index), S.length), 0); + var captures = []; + // NOTE: This is equivalent to + // captures = result.slice(1).map(maybeToString) + // but for some reason `nativeSlice.call(result, 1, result.length)` (called in + // the slice polyfill when slicing native arrays) "doesn't work" in safari 9 and + // causes a crash (https://pastebin.com/N21QzeQA) when trying to debug it. + for (var j = 1; j < result.length; j++) captures.push(maybeToString(result[j])); + var namedCaptures = result.groups; + if (functionalReplace) { + var replacerArgs = [matched].concat(captures, position, S); + if (namedCaptures !== undefined) replacerArgs.push(namedCaptures); + var replacement = String(replaceValue.apply(undefined, replacerArgs)); + } else { + replacement = getSubstitution(matched, S, position, captures, namedCaptures, replaceValue); + } + if (position >= nextSourcePosition) { + accumulatedResult += S.slice(nextSourcePosition, position) + replacement; + nextSourcePosition = position + matched.length; + } + } + return accumulatedResult + S.slice(nextSourcePosition); + } + ]; + + // https://tc39.github.io/ecma262/#sec-getsubstitution + function getSubstitution(matched, str, position, captures, namedCaptures, replacement) { + var tailPos = position + matched.length; + var m = captures.length; + var symbols = SUBSTITUTION_SYMBOLS_NO_NAMED; + if (namedCaptures !== undefined) { + namedCaptures = toObject(namedCaptures); + symbols = SUBSTITUTION_SYMBOLS; + } + return nativeReplace.call(replacement, symbols, function (match, ch) { + var capture; + switch (ch.charAt(0)) { + case '$': return '$'; + case '&': return matched; + case '`': return str.slice(0, position); + case "'": return str.slice(tailPos); + case '<': + capture = namedCaptures[ch.slice(1, -1)]; + break; + default: // \d\d? + var n = +ch; + if (n === 0) return match; + if (n > m) { + var f = floor$1(n / 10); + if (f === 0) return match; + if (f <= m) return captures[f - 1] === undefined ? ch.charAt(1) : captures[f - 1] + ch.charAt(1); + return match; + } + capture = captures[n - 1]; + } + return capture === undefined ? '' : capture; + }); + } + }); + + var MATCH = wellKnownSymbol('match'); + + // `IsRegExp` abstract operation + // https://tc39.github.io/ecma262/#sec-isregexp + var isRegexp = function (it) { + var isRegExp; + return isObject(it) && ((isRegExp = it[MATCH]) !== undefined ? !!isRegExp : classofRaw(it) == 'RegExp'); + }; + + var SPECIES$3 = wellKnownSymbol('species'); + + // `SpeciesConstructor` abstract operation + // https://tc39.github.io/ecma262/#sec-speciesconstructor + var speciesConstructor = function (O, defaultConstructor) { + var C = anObject(O).constructor; + var S; + return C === undefined || (S = anObject(C)[SPECIES$3]) == undefined ? defaultConstructor : aFunction$1(S); + }; + + var arrayPush = [].push; + var min$3 = Math.min; + var MAX_UINT32 = 0xFFFFFFFF; + + // babel-minify transpiles RegExp('x', 'y') -> /x/y and it causes SyntaxError + var SUPPORTS_Y = !fails(function () { return !RegExp(MAX_UINT32, 'y'); }); + + // @@split logic + fixRegexpWellKnownSymbolLogic('split', 2, function (SPLIT, nativeSplit, maybeCallNative) { + var internalSplit; + if ( + 'abbc'.split(/(b)*/)[1] == 'c' || + 'test'.split(/(?:)/, -1).length != 4 || + 'ab'.split(/(?:ab)*/).length != 2 || + '.'.split(/(.?)(.?)/).length != 4 || + '.'.split(/()()/).length > 1 || + ''.split(/.?/).length + ) { + // based on es5-shim implementation, need to rework it + internalSplit = function (separator, limit) { + var string = String(requireObjectCoercible(this)); + var lim = limit === undefined ? MAX_UINT32 : limit >>> 0; + if (lim === 0) return []; + if (separator === undefined) return [string]; + // If `separator` is not a regex, use native split + if (!isRegexp(separator)) { + return nativeSplit.call(string, separator, lim); + } + var output = []; + var flags = (separator.ignoreCase ? 'i' : '') + + (separator.multiline ? 'm' : '') + + (separator.unicode ? 'u' : '') + + (separator.sticky ? 'y' : ''); + var lastLastIndex = 0; + // Make `global` and avoid `lastIndex` issues by working with a copy + var separatorCopy = new RegExp(separator.source, flags + 'g'); + var match, lastIndex, lastLength; + while (match = regexpExec.call(separatorCopy, string)) { + lastIndex = separatorCopy.lastIndex; + if (lastIndex > lastLastIndex) { + output.push(string.slice(lastLastIndex, match.index)); + if (match.length > 1 && match.index < string.length) arrayPush.apply(output, match.slice(1)); + lastLength = match[0].length; + lastLastIndex = lastIndex; + if (output.length >= lim) break; + } + if (separatorCopy.lastIndex === match.index) separatorCopy.lastIndex++; // Avoid an infinite loop + } + if (lastLastIndex === string.length) { + if (lastLength || !separatorCopy.test('')) output.push(''); + } else output.push(string.slice(lastLastIndex)); + return output.length > lim ? output.slice(0, lim) : output; + }; + // Chakra, V8 + } else if ('0'.split(undefined, 0).length) { + internalSplit = function (separator, limit) { + return separator === undefined && limit === 0 ? [] : nativeSplit.call(this, separator, limit); + }; + } else internalSplit = nativeSplit; + + return [ + // `String.prototype.split` method + // https://tc39.github.io/ecma262/#sec-string.prototype.split + function split(separator, limit) { + var O = requireObjectCoercible(this); + var splitter = separator == undefined ? undefined : separator[SPLIT]; + return splitter !== undefined + ? splitter.call(separator, O, limit) + : internalSplit.call(String(O), separator, limit); + }, + // `RegExp.prototype[@@split]` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@split + // + // NOTE: This cannot be properly polyfilled in engines that don't support + // the 'y' flag. + function (regexp, limit) { + var res = maybeCallNative(internalSplit, regexp, this, limit, internalSplit !== nativeSplit); + if (res.done) return res.value; + + var rx = anObject(regexp); + var S = String(this); + var C = speciesConstructor(rx, RegExp); + + var unicodeMatching = rx.unicode; + var flags = (rx.ignoreCase ? 'i' : '') + + (rx.multiline ? 'm' : '') + + (rx.unicode ? 'u' : '') + + (SUPPORTS_Y ? 'y' : 'g'); + + // ^(? + rx + ) is needed, in combination with some S slicing, to + // simulate the 'y' flag. + var splitter = new C(SUPPORTS_Y ? rx : '^(?:' + rx.source + ')', flags); + var lim = limit === undefined ? MAX_UINT32 : limit >>> 0; + if (lim === 0) return []; + if (S.length === 0) return regexpExecAbstract(splitter, S) === null ? [S] : []; + var p = 0; + var q = 0; + var A = []; + while (q < S.length) { + splitter.lastIndex = SUPPORTS_Y ? q : 0; + var z = regexpExecAbstract(splitter, SUPPORTS_Y ? S : S.slice(q)); + var e; + if ( + z === null || + (e = min$3(toLength(splitter.lastIndex + (SUPPORTS_Y ? 0 : q)), S.length)) === p + ) { + q = advanceStringIndex(S, q, unicodeMatching); + } else { + A.push(S.slice(p, q)); + if (A.length === lim) return A; + for (var i = 1; i <= z.length - 1; i++) { + A.push(z[i]); + if (A.length === lim) return A; + } + q = p = e; + } + } + A.push(S.slice(p)); + return A; + } + ]; + }, !SUPPORTS_Y); + + // a string of all valid unicode whitespaces + // eslint-disable-next-line max-len + var whitespaces = '\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF'; + + var whitespace = '[' + whitespaces + ']'; + var ltrim = RegExp('^' + whitespace + whitespace + '*'); + var rtrim = RegExp(whitespace + whitespace + '*$'); + + // `String.prototype.{ trim, trimStart, trimEnd, trimLeft, trimRight }` methods implementation + var createMethod$3 = function (TYPE) { + return function ($this) { + var string = String(requireObjectCoercible($this)); + if (TYPE & 1) string = string.replace(ltrim, ''); + if (TYPE & 2) string = string.replace(rtrim, ''); + return string; + }; + }; + + var stringTrim = { + // `String.prototype.{ trimLeft, trimStart }` methods + // https://tc39.github.io/ecma262/#sec-string.prototype.trimstart + start: createMethod$3(1), + // `String.prototype.{ trimRight, trimEnd }` methods + // https://tc39.github.io/ecma262/#sec-string.prototype.trimend + end: createMethod$3(2), + // `String.prototype.trim` method + // https://tc39.github.io/ecma262/#sec-string.prototype.trim + trim: createMethod$3(3) + }; + + var non = '\u200B\u0085\u180E'; + + // check that a method works with the correct list + // of whitespaces and has a correct name + var forcedStringTrimMethod = function (METHOD_NAME) { + return fails(function () { + return !!whitespaces[METHOD_NAME]() || non[METHOD_NAME]() != non || whitespaces[METHOD_NAME].name !== METHOD_NAME; + }); + }; + + var $trim = stringTrim.trim; + + + // `String.prototype.trim` method + // https://tc39.github.io/ecma262/#sec-string.prototype.trim + _export({ target: 'String', proto: true, forced: forcedStringTrimMethod('trim') }, { + trim: function trim() { + return $trim(this); + } + }); + + // iterable DOM collections + // flag - `iterable` interface - 'entries', 'keys', 'values', 'forEach' methods + var domIterables = { + CSSRuleList: 0, + CSSStyleDeclaration: 0, + CSSValueList: 0, + ClientRectList: 0, + DOMRectList: 0, + DOMStringList: 0, + DOMTokenList: 1, + DataTransferItemList: 0, + FileList: 0, + HTMLAllCollection: 0, + HTMLCollection: 0, + HTMLFormElement: 0, + HTMLSelectElement: 0, + MediaList: 0, + MimeTypeArray: 0, + NamedNodeMap: 0, + NodeList: 1, + PaintRequestList: 0, + Plugin: 0, + PluginArray: 0, + SVGLengthList: 0, + SVGNumberList: 0, + SVGPathSegList: 0, + SVGPointList: 0, + SVGStringList: 0, + SVGTransformList: 0, + SourceBufferList: 0, + StyleSheetList: 0, + TextTrackCueList: 0, + TextTrackList: 0, + TouchList: 0 + }; + + var $forEach = arrayIteration.forEach; + + + // `Array.prototype.forEach` method implementation + // https://tc39.github.io/ecma262/#sec-array.prototype.foreach + var arrayForEach = sloppyArrayMethod('forEach') ? function forEach(callbackfn /* , thisArg */) { + return $forEach(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined); + } : [].forEach; + + for (var COLLECTION_NAME in domIterables) { + var Collection = global_1[COLLECTION_NAME]; + var CollectionPrototype = Collection && Collection.prototype; + // some Chrome versions have non-configurable methods on DOMTokenList + if (CollectionPrototype && CollectionPrototype.forEach !== arrayForEach) try { + createNonEnumerableProperty(CollectionPrototype, 'forEach', arrayForEach); + } catch (error) { + CollectionPrototype.forEach = arrayForEach; + } + } + + function _typeof(obj) { + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function (obj) { + return typeof obj; + }; + } else { + _typeof = function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + + return _typeof(obj); + } + + var Utils = $.fn.bootstrapTable.utils; + var searchControls = 'select, input:not([type="checkbox"]):not([type="radio"])'; + function getOptionsFromSelectControl(selectControl) { + return selectControl.get(selectControl.length - 1).options; + } + function getControlContainer(that) { + if (that.options.filterControlContainer) { + return $("".concat(that.options.filterControlContainer)); + } + + return that.$header; + } + function getSearchControls(that) { + return getControlContainer(that).find(searchControls); + } + function hideUnusedSelectOptions(selectControl, uniqueValues) { + var options = getOptionsFromSelectControl(selectControl); + + for (var i = 0; i < options.length; i++) { + if (options[i].value !== '') { + if (!uniqueValues.hasOwnProperty(options[i].value)) { + selectControl.find(Utils.sprintf('option[value=\'%s\']', options[i].value)).hide(); + } else { + selectControl.find(Utils.sprintf('option[value=\'%s\']', options[i].value)).show(); + } + } + } + } + function existOptionInSelectControl(selectControl, value) { + var options = getOptionsFromSelectControl(selectControl); + + for (var i = 0; i < options.length; i++) { + if (options[i].value === value.toString()) { + // The value is not valid to add + return true; + } + } // If we get here, the value is valid to add + + + return false; + } + function addOptionToSelectControl(selectControl, _value, text, selected) { + var value = _value === undefined || _value === null ? '' : _value.toString().trim(); + var $selectControl = $(selectControl.get(selectControl.length - 1)); + + if (!existOptionInSelectControl(selectControl, value)) { + var option = $("<option value=\"".concat(value, "\">").concat(text, "</option>")); + + if (value === selected) { + option.attr('selected', true); + } + + $selectControl.append(option); + } + } + function sortSelectControl(selectControl, orderBy) { + var $selectControl = $(selectControl.get(selectControl.length - 1)); + var $opts = $selectControl.find('option:gt(0)'); + + if (orderBy !== 'server') { + $opts.sort(function (a, b) { + return Utils.sort(a.textContent, b.textContent, orderBy === 'desc' ? -1 : 1); + }); + } + + $selectControl.find('option:gt(0)').remove(); + $selectControl.append($opts); + } + function fixHeaderCSS(_ref) { + var $tableHeader = _ref.$tableHeader; + $tableHeader.css('height', '89px'); + } + function getElementClass($element) { + return $element.attr('class').replace('form-control', '').replace('focus-temp', '').replace('search-input', '').trim(); + } + function getCursorPosition(el) { + if (Utils.isIEBrowser()) { + if ($(el).is('input[type=text]')) { + var pos = 0; + + if ('selectionStart' in el) { + pos = el.selectionStart; + } else if ('selection' in document) { + el.focus(); + var Sel = document.selection.createRange(); + var SelLength = document.selection.createRange().text.length; + Sel.moveStart('character', -el.value.length); + pos = Sel.text.length - SelLength; + } + + return pos; + } + + return -1; + } + + return -1; + } + function setCursorPosition(el) { + $(el).val(el.value); + } + function copyValues(that) { + var searchControls = getSearchControls(that); + that.options.valuesFilterControl = []; + searchControls.each(function () { + var $field = $(this); + + if (that.options.height) { + var fieldClass = getElementClass($field); + $field = $(".fixed-table-header .".concat(fieldClass)); + } + + that.options.valuesFilterControl.push({ + field: $field.closest('[data-field]').data('field'), + value: $field.val(), + position: getCursorPosition($field.get(0)), + hasFocus: $field.is(':focus') + }); + }); + } + function setValues(that) { + var field = null; + var result = []; + var searchControls = getSearchControls(that); + + if (that.options.valuesFilterControl.length > 0) { + // Callback to apply after settings fields values + var fieldToFocusCallback = null; + searchControls.each(function (index, ele) { + var $this = $(this); + field = $this.closest('[data-field]').data('field'); + result = that.options.valuesFilterControl.filter(function (valueObj) { + return valueObj.field === field; + }); + + if (result.length > 0) { + if ($this.is('[type=radio]')) { + return; + } + + $this.val(result[0].value); + + if (result[0].hasFocus && result[0].value !== '') { + // set callback if the field had the focus. + fieldToFocusCallback = function (fieldToFocus, carretPosition) { + // Closure here to capture the field and cursor position + var closedCallback = function closedCallback() { + fieldToFocus.focus(); + setCursorPosition(fieldToFocus); + }; + + return closedCallback; + }($this.get(0), result[0].position); + } + } + }); // Callback call. + + if (fieldToFocusCallback !== null) { + fieldToFocusCallback(); + } + } + } + function collectBootstrapCookies() { + var cookies = []; + var foundCookies = document.cookie.match(/(?:bs.table.)(\w*)/g); + var foundLocalStorage = localStorage; + + if (foundCookies) { + $.each(foundCookies, function (i, _cookie) { + var cookie = _cookie; + + if (/./.test(cookie)) { + cookie = cookie.split('.').pop(); + } + + if ($.inArray(cookie, cookies) === -1) { + cookies.push(cookie); + } + }); + } + + if (foundLocalStorage) { + for (var i = 0; i < foundLocalStorage.length; i++) { + var cookie = foundLocalStorage.key(i); + + if (/./.test(cookie)) { + cookie = cookie.split('.').pop(); + } + + if (!cookies.includes(cookie)) { + cookies.push(cookie); + } + } + } + + return cookies; + } + function escapeID(id) { + // eslint-disable-next-line no-useless-escape + return String(id).replace(/([:.\[\],])/g, '\\$1'); + } + function isColumnSearchableViaSelect(_ref2) { + var filterControl = _ref2.filterControl, + searchable = _ref2.searchable; + return filterControl && filterControl.toLowerCase() === 'select' && searchable; + } + function isFilterDataNotGiven(_ref3) { + var filterData = _ref3.filterData; + return filterData === undefined || filterData.toLowerCase() === 'column'; + } + function hasSelectControlElement(selectControl) { + return selectControl && selectControl.length > 0; + } + function initFilterSelectControls(that) { + var data = that.data; + var z = that.options.pagination ? that.options.sidePagination === 'server' ? that.pageTo : that.options.totalRows : that.pageTo; + $.each(that.header.fields, function (j, field) { + var column = that.columns[that.fieldsColumnsIndex[field]]; + var selectControl = getControlContainer(that).find("select.bootstrap-table-filter-control-".concat(escapeID(column.field))); + + if (isColumnSearchableViaSelect(column) && isFilterDataNotGiven(column) && hasSelectControlElement(selectControl)) { + if (selectControl.get(selectControl.length - 1).options.length === 0) { + // Added the default option + addOptionToSelectControl(selectControl, '', column.filterControlPlaceholder, column.filterDefault); + } + + var uniqueValues = {}; + + for (var i = 0; i < z; i++) { + // Added a new value + var fieldValue = data[i][field]; + var formatter = that.options.editable && column.editable ? column._formatter : that.header.formatters[j]; + var formattedValue = Utils.calculateObjectValue(that.header, formatter, [fieldValue, data[i], i], fieldValue); + + if (column.filterDataCollector) { + formattedValue = Utils.calculateObjectValue(that.header, column.filterDataCollector, [fieldValue, data[i], formattedValue], formattedValue); + } + + if (column.searchFormatter) { + fieldValue = formattedValue; + } + + uniqueValues[formattedValue] = fieldValue; + + if (_typeof(formattedValue) === 'object' && formattedValue !== null) { + formattedValue.forEach(function (value) { + addOptionToSelectControl(selectControl, value, value, column.filterDefault); + }); + continue; + } + + for (var key in uniqueValues) { + addOptionToSelectControl(selectControl, uniqueValues[key], key, column.filterDefault); + } + } + + sortSelectControl(selectControl, column.filterOrderBy); + + if (that.options.hideUnusedSelectOptions) { + hideUnusedSelectOptions(selectControl, uniqueValues); + } + } + }); + } + function getFilterDataMethod(objFilterDataMethod, searchTerm) { + var keys = Object.keys(objFilterDataMethod); + + for (var i = 0; i < keys.length; i++) { + if (keys[i] === searchTerm) { + return objFilterDataMethod[searchTerm]; + } + } + + return null; + } + function createControls(that, header) { + var addedFilterControl = false; + var html; + $.each(that.columns, function (_, column) { + html = []; + + if (!column.visible) { + return; + } + + if (!column.filterControl && !that.options.filterControlContainer) { + html.push('<div class="no-filter-control"></div>'); + } else if (that.options.filterControlContainer) { + var $filterControls = $(".bootstrap-table-filter-control-".concat(column.field)); + $.each($filterControls, function (_, filterControl) { + var $filterControl = $(filterControl); + + if (!$filterControl.is('[type=radio]')) { + var placeholder = column.filterControlPlaceholder ? column.filterControlPlaceholder : ''; + $filterControl.attr('placeholder', placeholder).val(column.filterDefault); + } + + $filterControl.attr('data-field', column.field); + }); + addedFilterControl = true; + } else { + var nameControl = column.filterControl.toLowerCase(); + html.push('<div class="filter-control">'); + addedFilterControl = true; + + if (column.searchable && that.options.filterTemplate[nameControl]) { + html.push(that.options.filterTemplate[nameControl](that, column.field, column.filterControlPlaceholder ? column.filterControlPlaceholder : '', column.filterDefault)); + } + } + + if (!column.filterControl && '' !== column.filterDefault && 'undefined' !== typeof column.filterDefault) { + if ($.isEmptyObject(that.filterColumnsPartial)) { + that.filterColumnsPartial = {}; + } + + that.filterColumnsPartial[column.field] = column.filterDefault; + } + + $.each(header.find('th'), function (i, th) { + var $th = $(th); + + if ($th.data('field') === column.field) { + $th.find('.fht-cell').append(html.join('')); + return false; + } + }); + + if (column.filterData && column.filterData.toLowerCase() !== 'column') { + var filterDataType = getFilterDataMethod( + /* eslint-disable no-use-before-define */ + filterDataMethods, column.filterData.substring(0, column.filterData.indexOf(':'))); + var filterDataSource; + var selectControl; + + if (filterDataType) { + filterDataSource = column.filterData.substring(column.filterData.indexOf(':') + 1, column.filterData.length); + selectControl = header.find(".bootstrap-table-filter-control-".concat(escapeID(column.field))); + addOptionToSelectControl(selectControl, '', column.filterControlPlaceholder, column.filterDefault); + filterDataType(filterDataSource, selectControl, that.options.filterOrderBy, column.filterDefault); + } else { + throw new SyntaxError('Error. You should use any of these allowed filter data methods: var, obj, json, url, func.' + ' Use like this: var: {key: "value"}'); + } + } + }); + + if (addedFilterControl) { + header.off('keyup', 'input').on('keyup', 'input', function (_ref4, obj) { + var currentTarget = _ref4.currentTarget, + keyCode = _ref4.keyCode; + syncControls(that); // Simulate enter key action from clear button + + keyCode = obj ? obj.keyCode : keyCode; + + if (that.options.searchOnEnterKey && keyCode !== 13) { + return; + } + + if ($.inArray(keyCode, [37, 38, 39, 40]) > -1) { + return; + } + + var $currentTarget = $(currentTarget); + + if ($currentTarget.is(':checkbox') || $currentTarget.is(':radio')) { + return; + } + + clearTimeout(currentTarget.timeoutId || 0); + currentTarget.timeoutId = setTimeout(function () { + that.onColumnSearch({ + currentTarget: currentTarget, + keyCode: keyCode + }); + }, that.options.searchTimeOut); + }); + header.off('change', 'select:not(".ms-offscreen")').on('change', 'select:not(".ms-offscreen")', function (_ref5) { + var currentTarget = _ref5.currentTarget, + keyCode = _ref5.keyCode; + syncControls(that); + var $select = $(currentTarget); + var value = $select.val(); + + if (value && value.length > 0 && value.trim()) { + $select.find('option[selected]').removeAttr('selected'); + $select.find('option[value="' + value + '"]').attr('selected', true); + } else { + $select.find('option[selected]').removeAttr('selected'); + } + + clearTimeout(currentTarget.timeoutId || 0); + currentTarget.timeoutId = setTimeout(function () { + that.onColumnSearch({ + currentTarget: currentTarget, + keyCode: keyCode + }); + }, that.options.searchTimeOut); + }); + header.off('mouseup', 'input:not([type=radio])').on('mouseup', 'input:not([type=radio])', function (_ref6) { + var currentTarget = _ref6.currentTarget, + keyCode = _ref6.keyCode; + var $input = $(currentTarget); + var oldValue = $input.val(); + + if (oldValue === '') { + return; + } + + setTimeout(function () { + syncControls(that); + var newValue = $input.val(); + + if (newValue === '') { + clearTimeout(currentTarget.timeoutId || 0); + currentTarget.timeoutId = setTimeout(function () { + that.onColumnSearch({ + currentTarget: currentTarget, + keyCode: keyCode + }); + }, that.options.searchTimeOut); + } + }, 1); + }); + header.off('change', 'input[type=radio]').on('change', 'input[type=radio]', function (_ref7) { + var currentTarget = _ref7.currentTarget, + keyCode = _ref7.keyCode; + clearTimeout(currentTarget.timeoutId || 0); + currentTarget.timeoutId = setTimeout(function () { + syncControls(that); + that.onColumnSearch({ + currentTarget: currentTarget, + keyCode: keyCode + }); + }, that.options.searchTimeOut); + }); + + if (header.find('.date-filter-control').length > 0) { + $.each(that.columns, function (i, _ref8) { + var filterControl = _ref8.filterControl, + field = _ref8.field, + filterDatepickerOptions = _ref8.filterDatepickerOptions; + + if (filterControl !== undefined && filterControl.toLowerCase() === 'datepicker') { + header.find(".date-filter-control.bootstrap-table-filter-control-".concat(field)).datepicker(filterDatepickerOptions).on('changeDate', function (_ref9) { + var currentTarget = _ref9.currentTarget, + keyCode = _ref9.keyCode; + clearTimeout(currentTarget.timeoutId || 0); + currentTarget.timeoutId = setTimeout(function () { + syncControls(that); + that.onColumnSearch({ + currentTarget: currentTarget, + keyCode: keyCode + }); + }, that.options.searchTimeOut); + }); + } + }); + } + + if (that.options.sidePagination !== 'server' && !that.options.height) { + that.triggerSearch(); + } + + if (!that.options.filterControlVisible) { + header.find('.filter-control, .no-filter-control').hide(); + } + } else { + header.find('.filter-control, .no-filter-control').hide(); + } + + that.trigger('created-controls'); + } + function getDirectionOfSelectOptions(_alignment) { + var alignment = _alignment === undefined ? 'left' : _alignment.toLowerCase(); + + switch (alignment) { + case 'left': + return 'ltr'; + + case 'right': + return 'rtl'; + + case 'auto': + return 'auto'; + + default: + return 'ltr'; + } + } + function syncControls(that) { + if (that.options.height) { + var controlsTableHeader = that.$tableHeader.find(searchControls); + that.$header.find(searchControls).each(function (_, control) { + var $control = $(control); + var controlClass = getElementClass($control); + var foundControl = controlsTableHeader.filter(function (_, ele) { + var eleClass = getElementClass($(ele)); + return controlClass === eleClass; + }); + + if (foundControl.length === 0) { + return; + } + + if ($control.is('select')) { + $control.find('option:selected').removeAttr('selected'); + $control.find("option[value='".concat(foundControl.val(), "']")).attr('selected', true); + } else { + $control.val(foundControl.val()); + } + }); + } + } + var filterDataMethods = { + func: function func(filterDataSource, selectControl, filterOrderBy, selected) { + var variableValues = window[filterDataSource].apply(); + + for (var key in variableValues) { + addOptionToSelectControl(selectControl, key, variableValues[key], selected); + } + + sortSelectControl(selectControl, filterOrderBy); + }, + obj: function obj(filterDataSource, selectControl, filterOrderBy, selected) { + var objectKeys = filterDataSource.split('.'); + var variableName = objectKeys.shift(); + var variableValues = window[variableName]; + + if (objectKeys.length > 0) { + objectKeys.forEach(function (key) { + variableValues = variableValues[key]; + }); + } + + for (var key in variableValues) { + addOptionToSelectControl(selectControl, key, variableValues[key], selected); + } + + sortSelectControl(selectControl, filterOrderBy); + }, + var: function _var(filterDataSource, selectControl, filterOrderBy, selected) { + var variableValues = window[filterDataSource]; + var isArray = Array.isArray(variableValues); + + for (var key in variableValues) { + if (isArray) { + addOptionToSelectControl(selectControl, variableValues[key], variableValues[key], selected); + } else { + addOptionToSelectControl(selectControl, key, variableValues[key], selected); + } + } + + sortSelectControl(selectControl, filterOrderBy); + }, + url: function url(filterDataSource, selectControl, filterOrderBy, selected) { + $.ajax({ + url: filterDataSource, + dataType: 'json', + success: function success(data) { + for (var key in data) { + addOptionToSelectControl(selectControl, key, data[key], selected); + } + + sortSelectControl(selectControl, filterOrderBy); + } + }); + }, + json: function json(filterDataSource, selectControl, filterOrderBy, selected) { + var variableValues = JSON.parse(filterDataSource); + + for (var key in variableValues) { + addOptionToSelectControl(selectControl, key, variableValues[key], selected); + } + + sortSelectControl(selectControl, filterOrderBy); + } + }; + + exports.addOptionToSelectControl = addOptionToSelectControl; + exports.collectBootstrapCookies = collectBootstrapCookies; + exports.copyValues = copyValues; + exports.createControls = createControls; + exports.escapeID = escapeID; + exports.existOptionInSelectControl = existOptionInSelectControl; + exports.fixHeaderCSS = fixHeaderCSS; + exports.getControlContainer = getControlContainer; + exports.getCursorPosition = getCursorPosition; + exports.getDirectionOfSelectOptions = getDirectionOfSelectOptions; + exports.getElementClass = getElementClass; + exports.getFilterDataMethod = getFilterDataMethod; + exports.getOptionsFromSelectControl = getOptionsFromSelectControl; + exports.getSearchControls = getSearchControls; + exports.hasSelectControlElement = hasSelectControlElement; + exports.hideUnusedSelectOptions = hideUnusedSelectOptions; + exports.initFilterSelectControls = initFilterSelectControls; + exports.isColumnSearchableViaSelect = isColumnSearchableViaSelect; + exports.isFilterDataNotGiven = isFilterDataNotGiven; + exports.setCursorPosition = setCursorPosition; + exports.setValues = setValues; + exports.sortSelectControl = sortSelectControl; + exports.syncControls = syncControls; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index db98617614..f9a52810a1 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -39,6 +39,7 @@ <link rel="stylesheet" href="{% static 'css/select2.css' %}"> <link rel="stylesheet" href="{% static 'css/select2-bootstrap.css' %}"> <link rel="stylesheet" href="{% static 'css/bootstrap-toggle.css' %}"> +<link rel="stylesheet" href="{% static 'css/bootstrap-table-filter-control.css' %}"> <link rel="stylesheet" href="{% static 'css/inventree.css' %}"> <link rel="stylesheet" href="{% get_color_theme_css user.get_username %}"> @@ -99,6 +100,8 @@ InvenTree <script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table-en-US.min.js' %}"></script> <script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table-group-by.js' %}"></script> <script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-toggle.js' %}"></script> +<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table-filter-control.js' %}"></script> +<!-- <script type='text/javascript' src="{% static 'script/bootstrap/filter-control-utils.js' %}"></script> --> <script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script> <script type='text/javascript' src="{% static 'script/moment.js' %}"></script> diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index bbdca8f8c4..3001790eac 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -210,6 +210,8 @@ function loadParametricPartTable(table, options={}) { field: header, title: header, sortable: true, + filterControl: 'input', + clear: 'fa-times icon-red', }); } } @@ -226,6 +228,7 @@ function loadParametricPartTable(table, options={}) { columns: columns, showColumns: true, data: table_data, + filterControl: true, }); } From 496232ed6d03af818471a35241780dc768959444 Mon Sep 17 00:00:00 2001 From: eeintech <eeintech@eeinte.ch> Date: Thu, 1 Oct 2020 13:46:56 -0500 Subject: [PATCH 08/77] Added tests for Category parameters methods, some code clean-up --- InvenTree/part/models.py | 2 +- InvenTree/part/test_category.py | 28 +++++++++++++++++++++++++++- InvenTree/templates/js/part.html | 12 ++++-------- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 272afacd0e..24c7ac1eb7 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -114,7 +114,7 @@ class PartCategory(InvenTreeTree): def prefetch_parts_parameters(self, cascade=True): """ Prefectch parts parameters """ - return self.get_parts(cascade=cascade).prefetch_related('parameters', 'parameters__template') + return self.get_parts(cascade=cascade).prefetch_related('parameters', 'parameters__template').all() def get_unique_parameters(self, cascade=True, prefetch=None): """ Get all unique parameter names for all parts from this category """ diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index 40f0f113a0..7fa38d7dcf 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -1,7 +1,7 @@ from django.test import TestCase from django.core.exceptions import ValidationError -from .models import Part, PartCategory +from .models import Part, PartCategory, PartParameter, PartParameterTemplate class CategoryTest(TestCase): @@ -15,6 +15,7 @@ class CategoryTest(TestCase): 'category', 'part', 'location', + 'params', ] def setUp(self): @@ -94,6 +95,31 @@ class CategoryTest(TestCase): self.assertEqual(self.electronics.item_count, self.electronics.partcount()) + def test_parameters(self): + """ Test that the Category parameters are correctly fetched """ + + # Check number of SQL queries to iterate other parameters + with self.assertNumQueries(3): + # Prefetch: 3 queries (parts, parameters and parameters_template) + fasteners = self.fasteners.prefetch_parts_parameters() + # Iterate through all parts and parameters + for fastener in fasteners: + self.assertIsInstance(fastener, Part) + for parameter in fastener.parameters.all(): + self.assertIsInstance(parameter, PartParameter) + self.assertIsInstance(parameter.template, PartParameterTemplate) + + # Test number of unique parameters + self.assertEqual(len(self.fasteners.get_unique_parameters(prefetch=fasteners)), 1) + # Test number of parameters found for each part + parts_parameters = self.fasteners.get_parts_parameters(prefetch=fasteners) + part_infos = ['pk', 'name', 'description'] + for part_parameter in parts_parameters: + # Remove part informations + for item in part_infos: + part_parameter.pop(item) + self.assertEqual(len(part_parameter), 1) + def test_invalid_name(self): # Test that an illegal character is prohibited in a category name diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index 3001790eac..e5fafef070 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -164,18 +164,16 @@ function loadSimplePartTable(table, url, options={}) { function loadParametricPartTable(table, options={}) { - /* Load parametric part data into specified table. + /* Load parametric table for part parameters * * Args: * - table: HTML reference to the table - * - table_headers: Table headers/columns + * - table_headers: Unique parameters found in category * - table_data: Parameters data */ var table_headers = options.headers var table_data = options.data -/* console.log(table_headers) - console.log(table_data)*/ var columns = []; @@ -211,19 +209,17 @@ function loadParametricPartTable(table, options={}) { title: header, sortable: true, filterControl: 'input', - clear: 'fa-times icon-red', + /* TODO: Search icons are not displayed */ + /*clear: 'fa-times icon-red',*/ }); } } $(table).inventreeTable({ -/* url: url,*/ sortName: 'part', -/* method: 'get',*/ queryParams: table_headers, groupBy: false, name: options.name || 'parametric', -/* original: params,*/ formatNoMatches: function() { return "{% trans "No parts found" %}"; }, columns: columns, showColumns: true, From f12f8156bd312cf5d40563f52fdaddeb10bd121a Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 2 Oct 2020 13:54:23 +1000 Subject: [PATCH 09/77] Fix for "next avilable serial number" string --- InvenTree/part/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 24c7ac1eb7..1400abd225 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -434,7 +434,7 @@ class Part(MPTTModel): return _('Next available serial numbers are') + ' ' + text else: - text = str(latest) + text = str(latest + 1) return _('Next available serial number is') + ' ' + text From c7403fd512094a4630848df3e64d62bb2bc3356d Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sat, 3 Oct 2020 16:18:03 +1000 Subject: [PATCH 10/77] Add shell interface --- InvenTree/InvenTree/settings.py | 1 + InvenTree/InvenTree/urls.py | 1 + requirements.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 7e398a9c34..f9e3285781 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -153,6 +153,7 @@ INSTALLED_APPS = [ 'markdownx', # Markdown editing 'markdownify', # Markdown template rendering 'django_tex', # LaTeX output + 'django_admin_shell', # Python shell for the admin interface ] LOGGING = { diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index c433cef382..d718871851 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -117,6 +117,7 @@ urlpatterns = [ url(r'^edit-user/', EditUserView.as_view(), name='edit-user'), url(r'^set-password/', SetPasswordView.as_view(), name='set-password'), + 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')), diff --git a/requirements.txt b/requirements.txt index 8faa7f58a5..6e634328ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,5 +25,6 @@ django-stdimage==5.1.1 # Advanced ImageField management django-tex==1.1.7 # LaTeX PDF export django-weasyprint==1.0.1 # HTML PDF export django-debug-toolbar==2.2 # Debug / profiling toolbar +django-admin-shell==0.1.2 # Python shell for the admin interface inventree # Install the latest version of the InvenTree API python library \ No newline at end of file From bedda66949e2812faa1270048b0ce8fe940cf75d Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sat, 3 Oct 2020 17:37:20 +1000 Subject: [PATCH 11/77] Add custom admin view for the "Group" model - Ref: https://github.com/Microdisseny/django-groupadmin-users - Adds ability to edit users within a particular group from the group admin page! --- InvenTree/InvenTree/settings.py | 1 + InvenTree/users/admin.py | 67 ++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index f9e3285781..21b8a0ead1 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -138,6 +138,7 @@ INSTALLED_APPS = [ 'part.apps.PartConfig', 'report.apps.ReportConfig', 'stock.apps.StockConfig', + 'users.apps.UsersConfig', # Third part add-ons 'django_filters', # Extended filter functionality diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index b12fa73f94..00e3ec7040 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -1,3 +1,66 @@ # -*- coding: utf-8 -*- -# from __future__ import unicode_literals -# from django.contrib import admin +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from django.contrib import admin +from django import forms +from django.contrib.auth import get_user_model +from django.contrib.admin.widgets import FilteredSelectMultiple +from django.contrib.auth.models import Group + +User = get_user_model() + + +class InvenTreeGroupAdminForm(forms.ModelForm): + + class Meta: + model = Group + exclude = [] + fields = [ + 'users', + 'permissions', + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if self.instance.pk: + # Populate the users field with the current Group users. + self.fields['users'].initial = self.instance.user_set.all() + + # Add the users field. + users = forms.ModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + widget=FilteredSelectMultiple('users', False), + label=_('Users'), + ) + + def save_m2m(self): + # Add the users to the Group. + # Deprecated in Django 1.10: Direct assignment to a reverse foreign key + # or many-to-many relation + + self.instance.user_set.set(self.cleaned_data['users']) + + def save(self, *args, **kwargs): + # Default save + instance = super().save() + # Save many-to-many data + self.save_m2m() + return instance + + +class RoleGroupAdmin(admin.ModelAdmin): + """ + Custom admin interface for the Group model + """ + + form = InvenTreeGroupAdminForm + + filter_horizontal = ['permissions'] + + +admin.site.unregister(Group) +admin.site.register(Group, RoleGroupAdmin) From 16f1b4c78437bf58772788527a28f774b52024b7 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sat, 3 Oct 2020 23:45:24 +1000 Subject: [PATCH 12/77] Add hook to update group permission roles (doesn't do anything yet) --- InvenTree/users/admin.py | 39 ++++++++++ InvenTree/users/apps.py | 25 ++++++ InvenTree/users/models.py | 159 ++++++++++++++++++++++++++++++++++++++ tasks.py | 3 +- 4 files changed, 225 insertions(+), 1 deletion(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index 00e3ec7040..741f898c81 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -11,6 +11,20 @@ from django.contrib.auth.models import Group User = get_user_model() +from users.models import RuleSet + + +class RuleSetInline(admin.TabularInline): + model = RuleSet + can_delete = False + verbose_name = 'Ruleset' + verbose_plural_name = 'Rulesets' + fields = ['name'] + [option for option in RuleSet.RULE_OPTIONS] + readonly_fields = ['name'] + max_num = len(RuleSet.RULESET_CHOICES) + min_num = 1 + extra = 0 + class InvenTreeGroupAdminForm(forms.ModelForm): @@ -18,6 +32,7 @@ class InvenTreeGroupAdminForm(forms.ModelForm): model = Group exclude = [] fields = [ + 'name', 'users', 'permissions', ] @@ -35,6 +50,7 @@ class InvenTreeGroupAdminForm(forms.ModelForm): required=False, widget=FilteredSelectMultiple('users', False), label=_('Users'), + help_text=_('Select which users are assigned to this group') ) def save_m2m(self): @@ -59,8 +75,31 @@ class RoleGroupAdmin(admin.ModelAdmin): form = InvenTreeGroupAdminForm + inlines = [ + RuleSetInline, + ] + + def get_formsets_with_inlines(self, request, obj=None): + for inline in self.get_inline_instances(request, obj): + # Hide RuleSetInline in the 'Add role' view + if not isinstance(inline, RuleSetInline) or obj is not None: + yield inline.get_formset(request, obj), inline + filter_horizontal = ['permissions'] + # Save inlines before model + # https://stackoverflow.com/a/14860703/12794913 + def save_model(self, request, obj, form, change): + if obj is not None: + # Save model immediately only if in 'Add role' view + super().save_model(request, obj, form, change) + else: + pass # don't actually save the parent instance + + def save_formset(self, request, form, formset, change): + formset.save() # this will save the children + form.instance.save() # form.instance is the parent + admin.site.unregister(Group) admin.site.register(Group, RoleGroupAdmin) diff --git a/InvenTree/users/apps.py b/InvenTree/users/apps.py index 251989770b..b352e54baf 100644 --- a/InvenTree/users/apps.py +++ b/InvenTree/users/apps.py @@ -1,8 +1,33 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.db.utils import OperationalError, ProgrammingError + from django.apps import AppConfig class UsersConfig(AppConfig): name = 'users' + + def ready(self): + + try: + self.assign_permissions() + except (OperationalError, ProgrammingError): + pass + + def assign_permissions(self): + + from django.contrib.auth.models import Group + from users.models import RuleSet, update_group_roles + + # First, delete any rule_set objects which have become outdated! + for rule in RuleSet.objects.all(): + if rule.name not in RuleSet.RULESET_NAMES: + print("need to delete:", rule.name) + rule.delete() + + # Update group permission assignments for all groups + for group in Group.objects.all(): + + update_group_roles(group) \ No newline at end of file diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 40a96afc6f..fd43e683e0 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -1 +1,160 @@ # -*- coding: utf-8 -*- + +from django.contrib.auth.models import Group +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from django.dispatch import receiver +from django.db.models.signals import post_save + + +class RuleSet(models.Model): + """ + A RuleSet is somewhat like a superset of the django permission class, + in that in encapsulates a bunch of permissions. + + There are *many* apps models used within InvenTree, + so it makes sense to group them into "roles". + + These roles translate (roughly) to the menu options available. + + Each role controls permissions for a number of database tables, + which are then handled using the normal django permissions approach. + """ + + RULESET_CHOICES = [ + ('general', _('General')), + ('admin', _('Admin')), + ('part', _('Parts')), + ('stock', _('Stock')), + ('build', _('Build Orders')), + ('supplier', _('Suppliers')), + ('purchase_order', _('Purchase Orders')), + ('customer', _('Customers')), + ('sales_order', _('Sales Orders')), + ] + + RULESET_NAMES = [ + choice[0] for choice in RULESET_CHOICES + ] + + RULESET_MODELS = { + 'general': [ + 'part.partstar', + ], + 'admin': [ + 'auth.group', + 'auth.user', + 'auth.permission', + 'authtoken.token', + ], + 'part': [ + 'part.part', + 'part.bomitem', + 'part.partcategory', + 'part.partattachment', + 'part.partsellpricebreak', + 'part.parttesttemplate', + 'part.partparametertemplate', + 'part.partparameter', + ], + 'stock': [ + 'stock.stockitem', + 'stock.stocklocation', + 'stock.stockitemattachment', + 'stock.stockitemtracking', + 'stock.stockitemtestresult', + ], + 'build': [ + 'part.part', + 'part.partcategory', + 'part.bomitem', + 'build.build', + 'build.builditem', + 'stock.stockitem', + 'stock.stocklocation', + ] + } + + RULE_OPTIONS = [ + 'can_view', + 'can_add', + 'can_change', + 'can_delete', + ] + + class Meta: + unique_together = ( + ('name', 'group'), + ) + + name = models.CharField( + max_length=50, + choices=RULESET_CHOICES, + blank=False, + help_text=_('Permission set') + ) + + group = models.ForeignKey( + Group, + related_name='rule_sets', + blank=False, null=False, + on_delete=models.CASCADE, + help_text=_('Group'), + ) + + can_view = models.BooleanField(verbose_name=_('View'), default=True, help_text=_('Permission to view items')) + + can_add = models.BooleanField(verbose_name=_('Create'), default=False, help_text=_('Permission to add items')) + + can_change = models.BooleanField(verbose_name=_('Update'), default=False, help_text=_('Permissions to edit items')) + + can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items')) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + + super().save(*args, **kwargs) + + def get_models(self): + + models = { + '' + } + +def update_group_roles(group): + """ + Update group roles: + + a) Ensure default roles are assigned to each group. + b) Ensure group permissions are correctly updated and assigned + """ + + # List of permissions which must be added to the group + permissions_to_add = [] + + # List of permissions which must be removed from the group + permissions_to_delete = [] + + # Get all the rulesets associated with this group + for r in RuleSet.RULESET_CHOICES: + + rulename = r[0] + + try: + ruleset = RuleSet.objects.get(group=group, name=rulename) + except RuleSet.DoesNotExist: + # Create the ruleset with default values (if it does not exist) + ruleset = RuleSet.objects.create(group=group, name=rulename) + + # TODO - Update permissions here + + # TODO - Update group permissions + + +@receiver(post_save, sender=Group) +def create_missing_rule_sets(sender, instance, **kwargs): + + update_group_roles(instance) \ No newline at end of file diff --git a/tasks.py b/tasks.py index 9948b470d1..df386633e9 100644 --- a/tasks.py +++ b/tasks.py @@ -22,7 +22,8 @@ def apps(): 'part', 'report', 'stock', - 'InvenTree' + 'InvenTree', + 'users', ] def localDir(): From 9e4cc73b1cba090275bdd86962c22aa821087504 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 00:01:18 +1000 Subject: [PATCH 13/77] Add migration files --- InvenTree/users/migrations/0001_initial.py | 31 ++++++++++++++++++++++ InvenTree/users/migrations/__init__.py | 0 2 files changed, 31 insertions(+) create mode 100644 InvenTree/users/migrations/0001_initial.py create mode 100644 InvenTree/users/migrations/__init__.py diff --git a/InvenTree/users/migrations/0001_initial.py b/InvenTree/users/migrations/0001_initial.py new file mode 100644 index 0000000000..04071c5a63 --- /dev/null +++ b/InvenTree/users/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 3.0.7 on 2020-10-03 13:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ] + + operations = [ + migrations.CreateModel( + name='RuleSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(choices=[('general', 'General'), ('admin', 'Admin'), ('part', 'Parts'), ('stock', 'Stock'), ('build', 'Build Orders'), ('supplier', 'Suppliers'), ('purchase_order', 'Purchase Orders'), ('customer', 'Customers'), ('sales_order', 'Sales Orders')], help_text='Permission set', max_length=50)), + ('can_view', models.BooleanField(default=True, help_text='Permission to view items', verbose_name='View')), + ('can_add', models.BooleanField(default=False, help_text='Permission to add items', verbose_name='Create')), + ('can_change', models.BooleanField(default=False, help_text='Permissions to edit items', verbose_name='Update')), + ('can_delete', models.BooleanField(default=False, help_text='Permission to delete items', verbose_name='Delete')), + ('group', models.ForeignKey(help_text='Group', on_delete=django.db.models.deletion.CASCADE, related_name='rule_sets', to='auth.Group')), + ], + options={ + 'unique_together': {('name', 'group')}, + }, + ), + ] diff --git a/InvenTree/users/migrations/__init__.py b/InvenTree/users/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 6bc5fe2497c7d17038e17758dcf0cddaa88b2567 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 00:03:10 +1000 Subject: [PATCH 14/77] Tab fix --- InvenTree/users/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index 741f898c81..9a6b324bce 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -87,7 +87,7 @@ class RoleGroupAdmin(admin.ModelAdmin): filter_horizontal = ['permissions'] - # Save inlines before model + # Save inlines before model # https://stackoverflow.com/a/14860703/12794913 def save_model(self, request, obj, form, change): if obj is not None: From 2039100d3e7359d31a97ba39274f414ec4ec82f6 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 00:24:48 +1000 Subject: [PATCH 15/77] Add some unit testing --- InvenTree/company/apps.py | 3 +- .../migrations/0019_auto_20200413_0642.py | 2 - InvenTree/part/apps.py | 2 +- InvenTree/users/models.py | 26 ++++++++--- InvenTree/users/tests.py | 46 ++++++++++++++++++- 5 files changed, 67 insertions(+), 12 deletions(-) diff --git a/InvenTree/company/apps.py b/InvenTree/company/apps.py index 0afb18f616..5f84ce507f 100644 --- a/InvenTree/company/apps.py +++ b/InvenTree/company/apps.py @@ -38,4 +38,5 @@ class CompanyConfig(AppConfig): company.image = None company.save() except (OperationalError, ProgrammingError): - print("Could not generate Company thumbnails") + # Getting here probably meant the database was in test mode + pass diff --git a/InvenTree/company/migrations/0019_auto_20200413_0642.py b/InvenTree/company/migrations/0019_auto_20200413_0642.py index c81dfd795a..c3c2f58ea0 100644 --- a/InvenTree/company/migrations/0019_auto_20200413_0642.py +++ b/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -24,7 +24,6 @@ def reverse_association(apps, schema_editor): # Exit if there are no SupplierPart objects # This crucial otherwise the unit test suite fails! if SupplierPart.objects.count() == 0: - print("No SupplierPart objects - skipping") return print("Reversing migration for manufacturer association") @@ -105,7 +104,6 @@ def associate_manufacturers(apps, schema_editor): # Exit if there are no SupplierPart objects # This crucial otherwise the unit test suite fails! if SupplierPart.objects.count() == 0: - print("No SupplierPart objects - skipping") return # Link a 'manufacturer_name' to a 'Company' diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py index 2137ec5d89..198e58e337 100644 --- a/InvenTree/part/apps.py +++ b/InvenTree/part/apps.py @@ -37,4 +37,4 @@ class PartConfig(AppConfig): part.image = None part.save() except (OperationalError, ProgrammingError): - print("Could not generate Part thumbnails") + pass diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index fd43e683e0..9395f3e1f2 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -28,9 +28,7 @@ class RuleSet(models.Model): ('part', _('Parts')), ('stock', _('Stock')), ('build', _('Build Orders')), - ('supplier', _('Suppliers')), ('purchase_order', _('Purchase Orders')), - ('customer', _('Customers')), ('sales_order', _('Sales Orders')), ] @@ -73,6 +71,21 @@ class RuleSet(models.Model): 'build.builditem', 'stock.stockitem', 'stock.stocklocation', + ], + 'purchase_order': [ + 'company.company', + 'company.supplierpart', + 'company.supplierpricebreak', + 'order.purchaseorder', + 'order.purchaseorderattachment', + 'order.purchaseorderlineitem', + ], + 'sales_order': [ + 'company.company', + 'order.salesorder', + 'order.salesorderattachment', + 'order.salesorderlineitem', + 'order.salesorderallocation', ] } @@ -119,10 +132,11 @@ class RuleSet(models.Model): super().save(*args, **kwargs) def get_models(self): + """ + Return the database tables / models that this ruleset covers. + """ - models = { - '' - } + return self.RULESET_MODELS.get(self.name, []) def update_group_roles(group): """ @@ -157,4 +171,4 @@ def update_group_roles(group): @receiver(post_save, sender=Group) def create_missing_rule_sets(sender, instance, **kwargs): - update_group_roles(instance) \ No newline at end of file + update_group_roles(instance) diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 57c7c1fe6b..3687762fba 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -1,4 +1,46 @@ # -*- coding: utf-8 -*- -# from __future__ import unicode_literals +from __future__ import unicode_literals + +from django.test import TestCase + +from users.models import RuleSet + + +class RuleSetModelTest(TestCase): + """ + Some simplistic tests to ensure the RuleSet model is setup correctly. + """ + + def test_ruleset_models(self): + + keys = RuleSet.RULESET_MODELS.keys() + + # Check if there are any rulesets which do not have models defined + + missing = [name for name in RuleSet.RULESET_NAMES if name not in keys] + + if len(missing) > 0: + print("The following rulesets do not have models assigned:") + for m in missing: + print("-", m) + + # Check if models have been defined for a ruleset which is incorrect + extra = [name for name in keys if name not in RuleSet.RULESET_NAMES] + + if len(extra) > 0: + print("The following rulesets have been improperly added to RULESET_MODELS:") + for e in extra: + print("-", e) + + # Check that each ruleset has models assigned + empty = [key for key in keys if len(RuleSet.RULESET_MODELS[key]) == 0] + + if len(empty) > 0: + print("The following rulesets have empty entries in RULESET_MODELS:") + for e in empty: + print("-", e) + + self.assertEqual(len(missing), 0) + self.assertEqual(len(extra), 0) + self.assertEqual(len(empty), 0) -# from django.test import TestCase From 6c2eb959a6a63356bfd38aac65db5f68c2ab52b5 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 00:34:22 +1000 Subject: [PATCH 16/77] More unit testing --- InvenTree/users/models.py | 72 +++++++++++++++++++-------------------- InvenTree/users/tests.py | 28 +++++++++++++++ 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 9395f3e1f2..403dc94291 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -38,54 +38,54 @@ class RuleSet(models.Model): RULESET_MODELS = { 'general': [ - 'part.partstar', + 'part_partstar', ], 'admin': [ - 'auth.group', - 'auth.user', - 'auth.permission', - 'authtoken.token', + 'auth_group', + 'auth_user', + 'auth_permission', + 'authtoken_token', ], 'part': [ - 'part.part', - 'part.bomitem', - 'part.partcategory', - 'part.partattachment', - 'part.partsellpricebreak', - 'part.parttesttemplate', - 'part.partparametertemplate', - 'part.partparameter', + 'part_part', + 'part_bomitem', + 'part_partcategory', + 'part_partattachment', + 'part_partsellpricebreak', + 'part_parttesttemplate', + 'part_partparametertemplate', + 'part_partparameter', ], 'stock': [ - 'stock.stockitem', - 'stock.stocklocation', - 'stock.stockitemattachment', - 'stock.stockitemtracking', - 'stock.stockitemtestresult', + 'stock_stockitem', + 'stock_stocklocation', + 'stock_stockitemattachment', + 'stock_stockitemtracking', + 'stock_stockitemtestresult', ], 'build': [ - 'part.part', - 'part.partcategory', - 'part.bomitem', - 'build.build', - 'build.builditem', - 'stock.stockitem', - 'stock.stocklocation', + 'part_part', + 'part_partcategory', + 'part_bomitem', + 'build_build', + 'build_builditem', + 'stock_stockitem', + 'stock_stocklocation', ], 'purchase_order': [ - 'company.company', - 'company.supplierpart', - 'company.supplierpricebreak', - 'order.purchaseorder', - 'order.purchaseorderattachment', - 'order.purchaseorderlineitem', + 'company_company', + 'part_supplierpart', + 'part_supplierpricebreak', + 'order_purchaseorder', + 'order_purchaseorderattachment', + 'order_purchaseorderlineitem', ], 'sales_order': [ - 'company.company', - 'order.salesorder', - 'order.salesorderattachment', - 'order.salesorderlineitem', - 'order.salesorderallocation', + 'company_company', + 'order_salesorder', + 'order_salesorderattachment', + 'order_salesorderlineitem', + 'order_salesorderallocation', ] } diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 3687762fba..6c088441c9 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.test import TestCase +from django.apps import apps from users.models import RuleSet @@ -44,3 +45,30 @@ class RuleSetModelTest(TestCase): self.assertEqual(len(extra), 0) self.assertEqual(len(empty), 0) + def test_model_names(self): + """ + Test that each model defined in the rulesets is valid, + based on the database schema! + """ + + available_models = apps.get_models() + + available_tables = [] + + for model in available_models: + table_name = model.objects.model._meta.db_table + available_tables.append(table_name) + + errors = 0 + + # Now check that each defined model is a valid table name + for key in RuleSet.RULESET_MODELS.keys(): + + models = RuleSet.RULESET_MODELS[key] + + for m in models: + if m not in available_tables: + print("{n} is not a valid database table".format(n=m)) + errors += 1 + + self.assertEqual(errors, 0) \ No newline at end of file From 1ded9e1fc0627dae362a14d70aae7b76aba31370 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 00:38:53 +1000 Subject: [PATCH 17/77] Add a warning showing which databases tables are not covered by defined rulesets --- InvenTree/users/models.py | 1 + InvenTree/users/tests.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 403dc94291..c419f7a25a 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -45,6 +45,7 @@ class RuleSet(models.Model): 'auth_user', 'auth_permission', 'authtoken_token', + 'users_ruleset', ], 'part': [ 'part_part', diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 6c088441c9..485c02542a 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -61,14 +61,30 @@ class RuleSetModelTest(TestCase): errors = 0 + assigned_models = [] + # Now check that each defined model is a valid table name for key in RuleSet.RULESET_MODELS.keys(): models = RuleSet.RULESET_MODELS[key] for m in models: + + assigned_models.append(m) + if m not in available_tables: print("{n} is not a valid database table".format(n=m)) errors += 1 - self.assertEqual(errors, 0) \ No newline at end of file + self.assertEqual(errors, 0) + + missing_models = [] + + for model in available_tables: + if model not in assigned_models: + missing_models.append(model) + + if len(missing_models) > 0: + print("WARNING: The following database models are not covered by the define RuleSet permissions:") + for m in missing_models: + print("-", m) \ No newline at end of file From c09b4980adc85981e06267ab5b70dde164459a80 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 00:43:02 +1000 Subject: [PATCH 18/77] PEP fixes --- InvenTree/users/admin.py | 4 ++-- InvenTree/users/apps.py | 4 ++-- InvenTree/users/models.py | 7 ++++--- InvenTree/users/tests.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index 9a6b324bce..a556e60080 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -9,10 +9,10 @@ from django.contrib.auth import get_user_model from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Group -User = get_user_model() - from users.models import RuleSet +User = get_user_model() + class RuleSetInline(admin.TabularInline): model = RuleSet diff --git a/InvenTree/users/apps.py b/InvenTree/users/apps.py index b352e54baf..07e303c1be 100644 --- a/InvenTree/users/apps.py +++ b/InvenTree/users/apps.py @@ -14,7 +14,7 @@ class UsersConfig(AppConfig): try: self.assign_permissions() except (OperationalError, ProgrammingError): - pass + pass def assign_permissions(self): @@ -30,4 +30,4 @@ class UsersConfig(AppConfig): # Update group permission assignments for all groups for group in Group.objects.all(): - update_group_roles(group) \ No newline at end of file + update_group_roles(group) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index c419f7a25a..a0ae03d803 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -71,7 +71,7 @@ class RuleSet(models.Model): 'build_build', 'build_builditem', 'stock_stockitem', - 'stock_stocklocation', + 'stock_stocklocation', ], 'purchase_order': [ 'company_company', @@ -139,6 +139,7 @@ class RuleSet(models.Model): return self.RULESET_MODELS.get(self.name, []) + def update_group_roles(group): """ Update group roles: @@ -148,10 +149,10 @@ def update_group_roles(group): """ # List of permissions which must be added to the group - permissions_to_add = [] + # permissions_to_add = [] # List of permissions which must be removed from the group - permissions_to_delete = [] + # permissions_to_delete = [] # Get all the rulesets associated with this group for r in RuleSet.RULESET_CHOICES: diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 485c02542a..b88867a271 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -87,4 +87,4 @@ class RuleSetModelTest(TestCase): if len(missing_models) > 0: print("WARNING: The following database models are not covered by the define RuleSet permissions:") for m in missing_models: - print("-", m) \ No newline at end of file + print("-", m) From d5c0c12d78528b165ee0d0a3d2cc28e02a7ac9e3 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 11:03:14 +1100 Subject: [PATCH 19/77] Add some more unit testing - ALL models must be covered by rulesets - Added a RULESET_IGNORE list for models we do not want permissions for --- InvenTree/users/models.py | 88 ++++++++++++++++++++++++++++++++++++++- InvenTree/users/tests.py | 8 ++-- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index a0ae03d803..8dd634acdf 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -90,6 +90,23 @@ class RuleSet(models.Model): ] } + # Database models we ignore permission sets for + RULESET_IGNORE = [ + # Core django models (not user configurable) + 'django_admin_log', + 'django_content_type', + 'django_session', + + # Models which currently do not require permissions + 'common_inventreesetting', + 'common_currency', + 'common_colortheme', + 'company_contact', + 'label_stockitemlabel', + 'report_testreport', + 'report_reportasset', + ] + RULE_OPTIONS = [ 'can_view', 'can_add', @@ -125,6 +142,21 @@ class RuleSet(models.Model): can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items')) + @staticmethod + def get_model_permission_string(model, permission): + """ + Construct the correctly formatted permission string, + given the app_model name, and the permission type. + """ + + app, model = model.split('_') + + return "{app}.{perm}_{model}".format( + app=app, + perm=permission, + model=model + ) + def __str__(self): return self.name @@ -148,11 +180,43 @@ def update_group_roles(group): b) Ensure group permissions are correctly updated and assigned """ + # List of permissions already associated with this group + group_permissions = '??????' + # List of permissions which must be added to the group - # permissions_to_add = [] + permissions_to_add = set() # List of permissions which must be removed from the group - # permissions_to_delete = [] + permissions_to_delete = set() + + def add_model(name, action, allowed): + """ + Add a new model to the pile: + + args: + name - The name of the model e.g. part_part + action - The permission action e.g. view + allowed - Whether or not the action is allowed + """ + + if not action in ['view', 'add', 'change', 'delete']: + raise ValueError("Action {a} is invalid".format(a=action)) + + permission_string = RuleSet.get_model_permission_string(model, action) + + if allowed: + + # An 'allowed' action is always preferenced over a 'forbidden' action + if permission_string in permissions_to_delete: + permissions_to_delete.remove(permission_string) + + permissions_to_add.add(permission_string) + + else: + + # A forbidden action will be ignored if we have already allowed it + if permission_string not in permissions_to_add: + permissions_to_delete.add(permission_string) # Get all the rulesets associated with this group for r in RuleSet.RULESET_CHOICES: @@ -165,12 +229,32 @@ def update_group_roles(group): # Create the ruleset with default values (if it does not exist) ruleset = RuleSet.objects.create(group=group, name=rulename) + # Which database tables does this RuleSet touch? + models = ruleset.get_models() + + for model in models: + # Keep track of the available permissions for each model + + add_model(model, 'view', ruleset.can_view) + add_model(model, 'add', ruleset.can_add) + add_model(model, 'change', ruleset.can_change) + add_model(model, 'delete', ruleset.can_delete) + # TODO - Update permissions here # TODO - Update group permissions + print("To add:", permissions_to_add) + print("To delete:", permissions_to_delete) + @receiver(post_save, sender=Group) def create_missing_rule_sets(sender, instance, **kwargs): + """ + Called *after* a Group object is saved. + As the linked RuleSet instances are saved *before* the Group, + then we can now use these RuleSet values to update the + group permissions. + """ update_group_roles(instance) diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index b88867a271..8db631ab98 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -76,15 +76,17 @@ class RuleSetModelTest(TestCase): print("{n} is not a valid database table".format(n=m)) errors += 1 - self.assertEqual(errors, 0) missing_models = [] for model in available_tables: - if model not in assigned_models: + if model not in assigned_models and model not in RuleSet.RULESET_IGNORE: missing_models.append(model) if len(missing_models) > 0: - print("WARNING: The following database models are not covered by the define RuleSet permissions:") + print("The following database models are not covered by the defined RuleSet permissions:") for m in missing_models: print("-", m) + + self.assertEqual(errors, 0) + self.assertEqual(len(missing_models), 0) \ No newline at end of file From c19c014f55e3b0db29ea03a7c32bb4f610178fe0 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 12:18:31 +1100 Subject: [PATCH 20/77] Add or remove permissions from groups as defined by the RuleSet links - Only runs when the group is changed - Does not add permissions if they already exist - Does not remove permissions if they do not exist --- InvenTree/users/models.py | 87 ++++++++++++++++++++++++++++++++------- InvenTree/users/tests.py | 3 +- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 8dd634acdf..1bffbf86a9 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import gettext_lazy as _ @@ -172,16 +173,35 @@ class RuleSet(models.Model): return self.RULESET_MODELS.get(self.name, []) -def update_group_roles(group): +def update_group_roles(group, debug=False): """ - Update group roles: - - a) Ensure default roles are assigned to each group. - b) Ensure group permissions are correctly updated and assigned + + Iterates through all of the RuleSets associated with the group, + and ensures that the correct permissions are either applied or removed from the group. + + This function is called under the following conditions: + + a) Whenever the InvenTree database is launched + b) Whenver the group object is updated + + The RuleSet model has complete control over the permissions applied to any group. + """ # List of permissions already associated with this group - group_permissions = '??????' + group_permissions = set() + + # Iterate through each permission already assigned to this group, + # and create a simplified permission key string + for p in group.permissions.all(): + (permission, app, model) = p.natural_key() + + permission_string = '{app}.{perm}'.format( + app=app, + perm=permission + ) + + group_permissions.add(permission_string) # List of permissions which must be added to the group permissions_to_add = set() @@ -199,7 +219,7 @@ def update_group_roles(group): allowed - Whether or not the action is allowed """ - if not action in ['view', 'add', 'change', 'delete']: + if action not in ['view', 'add', 'change', 'delete']: raise ValueError("Action {a} is invalid".format(a=action)) permission_string = RuleSet.get_model_permission_string(model, action) @@ -210,13 +230,16 @@ def update_group_roles(group): if permission_string in permissions_to_delete: permissions_to_delete.remove(permission_string) - permissions_to_add.add(permission_string) + if permission_string not in group_permissions: + permissions_to_add.add(permission_string) else: # A forbidden action will be ignored if we have already allowed it if permission_string not in permissions_to_add: - permissions_to_delete.add(permission_string) + + if permission_string in group_permissions: + permissions_to_delete.add(permission_string) # Get all the rulesets associated with this group for r in RuleSet.RULESET_CHOICES: @@ -240,12 +263,46 @@ def update_group_roles(group): add_model(model, 'change', ruleset.can_change) add_model(model, 'delete', ruleset.can_delete) - # TODO - Update permissions here + def get_permission_object(permission_string): + """ + Find the permission object in the database, + from the simplified permission string - # TODO - Update group permissions + Args: + permission_string - a simplified permission_string e.g. 'part.view_partcategory' - print("To add:", permissions_to_add) - print("To delete:", permissions_to_delete) + Returns the permission object in the database associated with the permission string + """ + + (app, perm) = permission_string.split('.') + + (permission_name, model) = perm.split('_') + + content_type = ContentType.objects.get(app_label=app, model=model) + + permission = Permission.objects.get(content_type=content_type, codename=perm) + + return permission + + # Add any required permissions to the group + for perm in permissions_to_add: + + permission = get_permission_object(perm) + + group.permissions.add(permission) + + if debug: + print(f"Adding permission {perm} to group {group.name}") + + # Remove any extra permissions from the group + for perm in permissions_to_delete: + + permission = get_permission_object(perm) + + group.permissions.remove(permission) + + if debug: + print(f"Removing permission {perm} from group {group.name}") @receiver(post_save, sender=Group) @@ -253,7 +310,7 @@ def create_missing_rule_sets(sender, instance, **kwargs): """ Called *after* a Group object is saved. As the linked RuleSet instances are saved *before* the Group, - then we can now use these RuleSet values to update the + then we can now use these RuleSet values to update the group permissions. """ diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 8db631ab98..ad3a6ec5ae 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -76,7 +76,6 @@ class RuleSetModelTest(TestCase): print("{n} is not a valid database table".format(n=m)) errors += 1 - missing_models = [] for model in available_tables: @@ -89,4 +88,4 @@ class RuleSetModelTest(TestCase): print("-", m) self.assertEqual(errors, 0) - self.assertEqual(len(missing_models), 0) \ No newline at end of file + self.assertEqual(len(missing_models), 0) From cda52a58e318c7f0aeed0cc1c50b2617418f9d5d Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 12:19:56 +1100 Subject: [PATCH 21/77] Remove manual 'permissions' control from groups admin interface - Does not actually *do* anything any more as the RuleSet approach overrides it anyway --- InvenTree/users/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index a556e60080..c9546e0335 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -34,7 +34,6 @@ class InvenTreeGroupAdminForm(forms.ModelForm): fields = [ 'name', 'users', - 'permissions', ] def __init__(self, *args, **kwargs): From 31b699d5216bb0df1ff64b813db78eecd14bc53b Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 12:47:19 +1100 Subject: [PATCH 22/77] Hide "user permissions" view from the admin interface --- InvenTree/users/admin.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index c9546e0335..86d4bb1a86 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -8,6 +8,7 @@ from django import forms from django.contrib.auth import get_user_model from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Group +from django.contrib.auth.admin import UserAdmin from users.models import RuleSet @@ -15,6 +16,10 @@ User = get_user_model() class RuleSetInline(admin.TabularInline): + """ + Class for displaying inline RuleSet data in the Group admin page. + """ + model = RuleSet can_delete = False verbose_name = 'Ruleset' @@ -27,6 +32,11 @@ class RuleSetInline(admin.TabularInline): class InvenTreeGroupAdminForm(forms.ModelForm): + """ + Custom admin form for the Group model. + + Adds the ability for editing user membership directly in the group admin page. + """ class Meta: model = Group @@ -54,8 +64,6 @@ class InvenTreeGroupAdminForm(forms.ModelForm): def save_m2m(self): # Add the users to the Group. - # Deprecated in Django 1.10: Direct assignment to a reverse foreign key - # or many-to-many relation self.instance.user_set.set(self.cleaned_data['users']) @@ -100,5 +108,28 @@ class RoleGroupAdmin(admin.ModelAdmin): form.instance.save() # form.instance is the parent +class InvenTreeUserAdmin(UserAdmin): + """ + Custom admin page for the User model. + + Hides the "permissions" view as this is now handled + entirely by groups and RuleSets. + + (And it's confusing!) + """ + + fieldsets = ( + (None, {'fields': ('username', 'password')}), + (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), + (_('Permissions'), { + 'fields': ('is_active', 'is_staff', 'is_superuser', 'groups'), + }), + (_('Important dates'), {'fields': ('last_login', 'date_joined')}), + ) + + admin.site.unregister(Group) admin.site.register(Group, RoleGroupAdmin) + +admin.site.unregister(User) +admin.site.register(User, InvenTreeUserAdmin) From 929411e49a96beea57b33cf1312d92a31b3829ea Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 12:53:24 +1100 Subject: [PATCH 23/77] Remove "general" ruleset --- InvenTree/users/models.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 1bffbf86a9..8f349e2f18 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -24,7 +24,6 @@ class RuleSet(models.Model): """ RULESET_CHOICES = [ - ('general', _('General')), ('admin', _('Admin')), ('part', _('Parts')), ('stock', _('Stock')), @@ -38,9 +37,6 @@ class RuleSet(models.Model): ] RULESET_MODELS = { - 'general': [ - 'part_partstar', - ], 'admin': [ 'auth_group', 'auth_user', @@ -99,13 +95,14 @@ class RuleSet(models.Model): 'django_session', # Models which currently do not require permissions - 'common_inventreesetting', - 'common_currency', 'common_colortheme', + 'common_currency', + 'common_inventreesetting', 'company_contact', 'label_stockitemlabel', - 'report_testreport', 'report_reportasset', + 'report_testreport', + 'part_partstar', ] RULE_OPTIONS = [ From fb09f53dc9a2016cd472ac5c63a6202b930ae616 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 12:58:45 +1100 Subject: [PATCH 24/77] Add missing migration file --- .../migrations/0002_auto_20201004_0158.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 InvenTree/users/migrations/0002_auto_20201004_0158.py diff --git a/InvenTree/users/migrations/0002_auto_20201004_0158.py b/InvenTree/users/migrations/0002_auto_20201004_0158.py new file mode 100644 index 0000000000..a0573a89be --- /dev/null +++ b/InvenTree/users/migrations/0002_auto_20201004_0158.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-10-04 01:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='ruleset', + name='name', + field=models.CharField(choices=[('admin', 'Admin'), ('part', 'Parts'), ('stock', 'Stock'), ('build', 'Build Orders'), ('purchase_order', 'Purchase Orders'), ('sales_order', 'Sales Orders')], help_text='Permission set', max_length=50), + ), + ] From b27f9263103dcf958222e43f81a4d5bad7e20b6f Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 13:51:52 +1100 Subject: [PATCH 25/77] Add ability to filter BOM API by "trackable" status of the sub_part object --- InvenTree/part/api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index c70ef5c21a..9a86bb98d5 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -777,6 +777,13 @@ class BomList(generics.ListCreateAPIView): if sub_part is not None: queryset = queryset.filter(sub_part=sub_part) + # Filter by "trackable" status of the sub-part + trackable = self.request.query_params.get('trackable', None) + + if trackable is not None: + trackable = str2bool(trackable) + queryset = queryset.filter(sub_part__trackable=trackable) + return queryset permission_classes = [ From b467c8a1ef3e53801601711e3da9513e0af1b96a Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 15:17:46 +1100 Subject: [PATCH 26/77] Add front-end functions to render an "installed stock" table --- .../static/script/inventree/tables.js | 16 ++- InvenTree/templates/js/stock.html | 110 ++++++++++++++++++ 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/tables.js b/InvenTree/InvenTree/static/script/inventree/tables.js index cc4320307b..6d57240979 100644 --- a/InvenTree/InvenTree/static/script/inventree/tables.js +++ b/InvenTree/InvenTree/static/script/inventree/tables.js @@ -109,10 +109,20 @@ $.fn.inventreeTable = function(options) { options.pagination = true; options.pageSize = inventreeLoad(varName, 25); options.pageList = [25, 50, 100, 250, 'all']; + options.rememberOrder = true; - options.sortable = true; - options.search = true; - options.showColumns = true; + + if (options.sortable == null) { + options.sortable = true; + } + + if (options.search == null) { + options.search = true; + } + + if (options.showColumns == null) { + options.showColumns = true; + } // Callback to save pagination data options.onPageChange = function(number, size) { diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html index dd99c5b611..4ec2507b31 100644 --- a/InvenTree/templates/js/stock.html +++ b/InvenTree/templates/js/stock.html @@ -798,4 +798,114 @@ function createNewStockItem(options) { ]; launchModalForm("{% url 'stock-item-create' %}", options); +} + + +function loadInstalledInTable(table, options) { + /* + * Display a table showing the stock items which are installed in this stock item. + * This is a multi-level tree table, where the "top level" items are Part objects, + * and the children of each top-level item are the associated installed stock items. + * + * The process for retrieving data and displaying the table is as follows: + * + * A) Get BOM data for the stock item + * - It is assumed that the stock item will be for an assembly + * (otherwise why are we installing stuff anyway?) + * - Request BOM items for stock_item.part (and only for trackable sub items) + * + * B) Add parts to table + * - Create rows for each trackable sub-part in the table + * + * C) Gather installed stock item data + * - Get the list of installed stock items via the API + * - If the Part reference is already in the table, add the sub-item as a child + * - If this is a stock item for a *new* part, request that part from the API, + * and add that part as a new row, then add the stock item as a child of that part + * + * D) Enjoy! + * + * + * And the options object contains the following things: + * + * - stock_item: The PK of the master stock_item object + * - part: The PK of the Part reference of the stock_item object + */ + + table.inventreeTable( + { + url: "{% url 'api-bom-list' %}", + queryParams: { + part: options.part, + trackable: true, + sub_part_detail: true, + }, + showColumns: false, + name: 'installed-in', + columns: [ + { + checkbox: true, + title: '{% trans 'Select' %}', + searchable: false, + switchable: false, + }, + { + field: 'pk', + title: 'ID', + visible: false, + switchable: false, + }, + { + field: 'part', + title: '{% trans "Part" %}', + sortable: true, + formatter: function(value, row, index, field) { + + var url = `/stock/item/${row.pk}/`; + var thumb = row.sub_part_detail.thumbnail; + var name = row.sub_part_detail.full_name; + + html = imageHoverIcon(thumb) + renderLink(name, url); + + return html; + } + }, + { + field: 'installed', + title: '{% trans "Installed" %}', + sortable: false, + formatter: function(value, row, index, field) { + // Construct a progress showing how many items have been installed + + var installed = row.installed || 0; + var required = row.quantity || 0; + + var progress = makeProgressBar(installed, required, { + id: row.sub_part.pk, + }); + + return progress; + } + }, + { + field: 'actions', + switchable: false, + formatter: function(value, row) { + var pk = row.sub_part.pk; + + var html = `<div class='btn-group float-right' role='group'>`; + + html += makeIconButton('fa-link', 'button-install', pk, '{% trans "Install item" %}'); + + html += `</div>`; + + return html; + } + } + ], + onLoadSuccess: function() { + console.log('data loaded!'); + } + } + ); } \ No newline at end of file From f04977e7e12dd7f6ba17abf6bbab5a017b1c0ab8 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 20:41:28 +1100 Subject: [PATCH 27/77] Add form / view for installing a stock item into another stock item --- InvenTree/stock/forms.py | 28 ++++++++++++++++++++++++++++ InvenTree/stock/urls.py | 1 + InvenTree/stock/views.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index a5c689a605..ebcf31cb23 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from django import forms from django.forms.utils import ErrorDict from django.utils.translation import ugettext as _ +from django.core.validators import MinValueValidator from mptt.fields import TreeNodeChoiceField @@ -271,6 +272,33 @@ class ExportOptionsForm(HelperForm): self.fields['file_format'].choices = self.get_format_choices() +class InstallStockForm(HelperForm): + """ + Form for manually installing a stock item into another stock item + """ + + stock_item = forms.ModelChoiceField( + required=True, + queryset=StockItem.objects.filter(StockItem.IN_STOCK_FILTER), + help_text=_('Stock item to install') + ) + + quantity = RoundingDecimalFormField( + max_digits=10, decimal_places=5, + help_text=_('Stock quantity to assign'), + validators=[ + MinValueValidator(0.001) + ] + ) + + class Meta: + model = StockItem + fields = [ + 'stock_item', + 'quantity', + ] + + class UninstallStockForm(forms.ModelForm): """ Form for uninstalling a stock item which is installed in another item. diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 4c86995cda..7ad8bc4f7f 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -25,6 +25,7 @@ stock_item_detail_urls = [ url(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'), url(r'^assign/', views.StockItemAssignToCustomer.as_view(), name='stock-item-assign'), url(r'^return/', views.StockItemReturnToStock.as_view(), name='stock-item-return'), + url(r'^install/', views.StockItemInstall.as_view(), name='stock-item-install'), url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index c09c328c66..d818e82aa9 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -683,6 +683,46 @@ class StockItemQRCode(QRCodeView): return None +class StockItemInstall(AjaxUpdateView): + """ + View for manually installing stock items into + a particular stock item. + + In contrast to the StockItemUninstall view, + only a single stock item can be installed at once. + + The "part" to be installed must be provided in the GET query parameters. + + """ + + model = StockItem + form_class = StockForms.InstallStockForm + ajax_form_title = _('Install Stock Item') + + def get_form(self): + + form = super().get_form() + + return form + + def post(self, request, *args, **kwargs): + + + form = self.get_form() + valid = False + + valid = form.is_valid() and valid + + if valid: + pass + + data = { + 'form_valid': valid, + } + + return self.renderJsonResponse(request, form, data=data) + + class StockItemUninstall(AjaxView, FormMixin): """ View for uninstalling one or more StockItems, From fd22e713ff8359749be91e091815ccc1ed95a80c Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 20:50:06 +1100 Subject: [PATCH 28/77] Filter available stock items by Part reference --- InvenTree/stock/forms.py | 6 ++++-- InvenTree/stock/views.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index ebcf31cb23..28d0b47ef3 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -283,8 +283,10 @@ class InstallStockForm(HelperForm): help_text=_('Stock item to install') ) - quantity = RoundingDecimalFormField( + quantity_to_install = RoundingDecimalFormField( max_digits=10, decimal_places=5, + initial=1, + label=_('Quantity'), help_text=_('Stock quantity to assign'), validators=[ MinValueValidator(0.001) @@ -295,7 +297,7 @@ class InstallStockForm(HelperForm): model = StockItem fields = [ 'stock_item', - 'quantity', + 'quantity_to_install', ] diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index d818e82aa9..1f6ddcc0f4 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -703,6 +703,22 @@ class StockItemInstall(AjaxUpdateView): form = super().get_form() + queryset = form.fields['stock_item'].queryset + + part = self.request.GET.get('part', None) + + # Filter the available stock items based on the Part reference + if part: + try: + part = Part.objects.get(pk=part) + + queryset = queryset.filter(part=part) + + except (ValueError, Part.DoesNotExist): + pass + + form.fields['stock_item'].queryset = queryset + return form def post(self, request, *args, **kwargs): From a686500df15a5315389d5b63a85a2a611884eb57 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 21:02:20 +1100 Subject: [PATCH 29/77] Calculate initial values for the view --- InvenTree/stock/models.py | 3 ++- InvenTree/stock/views.py | 57 ++++++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index df1a628f47..62b774dd06 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -600,12 +600,13 @@ class StockItem(MPTTModel): return self.installedItemCount() > 0 @transaction.atomic - def installIntoStockItem(self, otherItem, user, notes): + def installIntoStockItem(self, otherItem, quantity, user, notes): """ Install this stock item into another stock item. Args otherItem: The stock item to install this item into + quantity: The quantity of stock to install user: The user performing the operation notes: Any notes associated with the operation """ diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 1f6ddcc0f4..541c60b2f5 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -699,25 +699,52 @@ class StockItemInstall(AjaxUpdateView): form_class = StockForms.InstallStockForm ajax_form_title = _('Install Stock Item') + def get_stock_items(self): + """ + Return a list of stock items suitable for displaying to the user. + + Requirements: + - Items must be in stock + + Filters: + - Items can be filtered by Part reference + """ + + items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER) + + # Filter by Part association + try: + part = self.request.GET.get('part', None) + + print(self.request.GET) + + if part is not None: + part = Part.objects.get(pk=part) + items = items.filter(part=part) + except (ValueError, Part.DoesNotExist): + pass + + return items + + def get_initial(self): + + initials = super().get_initial() + + items = self.get_stock_items() + + # If there is a single stock item available, we can use it! + if items.count() == 1: + item = items.first() + initials['stock_item'] = item.pk + initials['quantity_to_install'] = item.quantity + + return initials + def get_form(self): form = super().get_form() - queryset = form.fields['stock_item'].queryset - - part = self.request.GET.get('part', None) - - # Filter the available stock items based on the Part reference - if part: - try: - part = Part.objects.get(pk=part) - - queryset = queryset.filter(part=part) - - except (ValueError, Part.DoesNotExist): - pass - - form.fields['stock_item'].queryset = queryset + form.fields['stock_item'].queryset = self.get_stock_items() return form From 45c888e13d2df97512d688f2e00f84dafc6e7e3c Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 21:31:44 +1100 Subject: [PATCH 30/77] Custom cleaning for form Ok, looks like I've been doing this wrong the whole time! The "djangonic" way is pretty cool --- InvenTree/stock/forms.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 28d0b47ef3..729831a9ec 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -9,6 +9,7 @@ from django import forms from django.forms.utils import ErrorDict from django.utils.translation import ugettext as _ from django.core.validators import MinValueValidator +from django.core.exceptions import ValidationError from mptt.fields import TreeNodeChoiceField @@ -293,13 +294,33 @@ class InstallStockForm(HelperForm): ] ) + notes = forms.CharField( + required=False, + help_text=_('Notes') + ) + class Meta: model = StockItem fields = [ 'stock_item', 'quantity_to_install', + 'notes', ] + def clean(self): + + data = super().clean() + + print("Data:", data) + + stock_item = data['stock_item'] + quantity = data['quantity_to_install'] + + if quantity > stock_item.quantity: + raise ValidationError({'quantity_to_install': _('Must not exceed available quantity')}) + + return data + class UninstallStockForm(forms.ModelForm): """ From 9c27680202da2a0acd9a05c474ac58620b761e1e Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 21:32:21 +1100 Subject: [PATCH 31/77] Finish function to install stock item(s) --- InvenTree/stock/models.py | 31 +++++++++++++++++-------------- InvenTree/stock/views.py | 18 ++++++++++++------ 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 62b774dd06..964ab43e8a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -600,12 +600,12 @@ class StockItem(MPTTModel): return self.installedItemCount() > 0 @transaction.atomic - def installIntoStockItem(self, otherItem, quantity, user, notes): + def installStockItem(self, otherItem, quantity, user, notes): """ - Install this stock item into another stock item. + Install another stock item into this stock item. Args - otherItem: The stock item to install this item into + otherItem: The stock item to install into this stock item quantity: The quantity of stock to install user: The user performing the operation notes: Any notes associated with the operation @@ -615,16 +615,19 @@ class StockItem(MPTTModel): if self.belongs_to is not None: return False - # TODO - Are there any other checks that need to be performed at this stage? + # If the quantity is less than the stock item, split the stock! + stock_item = otherItem.splitStock(quantity, None, user) - # Mark this stock item as belonging to the other one - self.belongs_to = otherItem - - self.save() + if stock_item is None: + stock_item = otherItem + + # Assign the other stock item into this one + stock_item.belongs_to = self + stock_item.save() # Add a transaction note! - self.addTransactionNote( - _('Installed in stock item') + ' ' + str(otherItem.pk), + stock_item.addTransactionNote( + _('Installed into stock item') + ' ' + str(self.pk), user, notes=notes ) @@ -839,20 +842,20 @@ class StockItem(MPTTModel): # Do not split a serialized part if self.serialized: - return + return self try: quantity = Decimal(quantity) except (InvalidOperation, ValueError): - return + return self # Doesn't make sense for a zero quantity if quantity <= 0: - return + return self # Also doesn't make sense to split the full amount if quantity >= self.quantity: - return + return self # Create a new StockItem object, duplicating relevant fields # Nullify the PK so a new record is created diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 541c60b2f5..fe2ffbe7c6 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -716,8 +716,6 @@ class StockItemInstall(AjaxUpdateView): try: part = self.request.GET.get('part', None) - print(self.request.GET) - if part is not None: part = Part.objects.get(pk=part) items = items.filter(part=part) @@ -750,14 +748,22 @@ class StockItemInstall(AjaxUpdateView): def post(self, request, *args, **kwargs): - form = self.get_form() - valid = False - valid = form.is_valid() and valid + valid = form.is_valid() if valid: - pass + # We assume by this point that we have a valid stock_item and quantity values + data = form.cleaned_data + + other_stock_item = data['stock_item'] + quantity = data['quantity_to_install'] + notes = data['notes'] + + # Install the other stock item into this one + this_stock_item = self.get_object() + + this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes) data = { 'form_valid': valid, From 3c5968ef1ae21ddb3afd3ddc752b0e1cbdcaa28b Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 22:58:41 +1100 Subject: [PATCH 32/77] Add subrow table to the "installed items" view Ah, javascript... --- .../stock/templates/stock/item_installed.html | 151 +--------------- InvenTree/templates/js/stock.html | 170 +++++++++++++++++- 2 files changed, 174 insertions(+), 147 deletions(-) diff --git a/InvenTree/stock/templates/stock/item_installed.html b/InvenTree/stock/templates/stock/item_installed.html index cac55c9dce..2a6a0db057 100644 --- a/InvenTree/stock/templates/stock/item_installed.html +++ b/InvenTree/stock/templates/stock/item_installed.html @@ -10,19 +10,7 @@ <h4>{% trans "Installed Stock Items" %}</h4> <hr> -<div id='button-toolbar'> - <div class='button-toolbar container-fluid' style='float: right;'> - <div class="btn-group"> - <button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button> - <ul class="dropdown-menu"> - <li><a href="#" id='multi-item-uninstall' title='{% trans "Uninstall selected stock items" %}'>{% trans "Uninstall" %}</a></li> - </ul> - </div> - </div> -</div> - -<table class='table table-striped table-condensed' id='installed-table' data-toolbar='#button-toolbar'> -</table> +<table class='table table-striped table-condensed' id='installed-table'></table> {% endblock %} @@ -30,135 +18,14 @@ {{ block.super }} -$('#installed-table').inventreeTable({ - formatNoMatches: function() { - return '{% trans "No stock items installed" %}'; - }, - url: "{% url 'api-stock-list' %}", - queryParams: { - installed_in: {{ item.id }}, - part_detail: true, - }, - name: 'stock-item-installed', - url: "{% url 'api-stock-list' %}", - showColumns: true, - columns: [ - { - checkbox: true, - title: '{% trans 'Select' %}', - searchable: false, - switchable: false, - }, - { - field: 'pk', - title: 'ID', - visible: false, - switchable: false, - }, - { - field: 'part_name', - title: '{% trans "Part" %}', - sortable: true, - formatter: function(value, row, index, field) { - - var url = `/stock/item/${row.pk}/`; - var thumb = row.part_detail.thumbnail; - var name = row.part_detail.full_name; - - html = imageHoverIcon(thumb) + renderLink(name, url); - - return html; - } - }, - { - field: 'IPN', - title: 'IPN', - sortable: true, - formatter: function(value, row, index, field) { - return row.part_detail.IPN; - }, - }, - { - field: 'part_description', - title: '{% trans "Description" %}', - sortable: true, - formatter: function(value, row, index, field) { - return row.part_detail.description; - } - }, - { - field: 'quantity', - title: '{% trans "Stock" %}', - sortable: true, - formatter: function(value, row, index, field) { - - var val = parseFloat(value); - - // If there is a single unit with a serial number, use the serial number - if (row.serial && row.quantity == 1) { - val = '# ' + row.serial; - } else { - val = +val.toFixed(5); - } - - var html = renderLink(val, `/stock/item/${row.pk}/`); - - return html; - } - }, - { - field: 'status', - title: '{% trans "Status" %}', - sortable: 'true', - formatter: function(value, row, index, field) { - return stockStatusDisplay(value); - }, - }, - { - field: 'batch', - title: '{% trans "Batch" %}', - sortable: true, - }, - { - field: 'actions', - switchable: false, - title: '', - formatter: function(value, row) { - var pk = row.pk; - - var html = `<div class='btn-group float-right' role='group'>`; - - html += makeIconButton('fa-unlink', 'button-uninstall', pk, '{% trans "Uninstall item" %}'); - - html += `</div>`; - - return html; - } - } - ], - onLoadSuccess: function() { - - var table = $('#installed-table'); - - // Find buttons and associate actions - table.find('.button-uninstall').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm( - "{% url 'stock-item-uninstall' %}", - { - data: { - 'items[]': [pk], - }, - reload: true, - } - ); - }); - }, - buttons: [ - '#stock-options', - ] -}); +loadInstalledInTable( + $('#installed-table'), + { + stock_item: {{ item.pk }}, + part: {{ item.part.pk }}, + quantity: {{ item.quantity }}, + } +); $('#multi-item-uninstall').click(function() { diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html index 4ec2507b31..330be924e5 100644 --- a/InvenTree/templates/js/stock.html +++ b/InvenTree/templates/js/stock.html @@ -830,8 +830,25 @@ function loadInstalledInTable(table, options) { * * - stock_item: The PK of the master stock_item object * - part: The PK of the Part reference of the stock_item object + * - quantity: The quantity of the stock item */ + function updateCallbacks() { + // Setup callback functions when buttons are pressed + table.find('.button-install').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/stock/item/${options.stock_item}/install/`, + { + data: { + part: pk, + }, + } + ); + }); + } + table.inventreeTable( { url: "{% url 'api-bom-list' %}", @@ -842,6 +859,92 @@ function loadInstalledInTable(table, options) { }, showColumns: false, name: 'installed-in', + detailView: true, + detailViewByClick: true, + detailFilter: function(index, row) { + return row.installed_count && row.installed_count > 0; + }, + detailFormatter: function(index, row, element) { + var subTableId = `installed-table-${row.sub_part}`; + + var html = `<div class='sub-table'><table class='table table-condensed table-striped' id='${subTableId}'></table></div>`; + + element.html(html); + + var subTable = $(`#${subTableId}`); + + // Display a "sub table" showing all the linked stock items + subTable.bootstrapTable({ + data: row.installed_items, + showHeader: true, + columns: [ + { + field: 'item', + title: '{% trans "Stock Item" %}', + formatter: function(value, subrow, index, field) { + + var pk = subrow.pk; + var html = ''; + + html += row.sub_part_detail.full_name; + html += " | "; + + if (subrow.serial && subrow.quantity == 1) { + html += `{% trans "Serial" %}: ${subrow.serial}`; + } else { + html += `{% trans "Quantity" %}: ${subrow.quantity}`; + } + + return html; + }, + }, + { + field: 'status', + title: '{% trans "Status" %}', + formatter: function(value, subrow, index, field) { + return stockStatusDisplay(value); + } + }, + { + field: 'actions', + title: '', + formatter: function(value, subrow, index) { + + var pk = subrow.pk; + var html = ''; + + // Add some buttons yo! + html += `<div class='btn-group float-right' role='group'>`; + + html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans "Uninstall stock item" %}"); + + html += `</div>`; + + return html; + } + } + ], + onPostBody: function() { + // Setup button callbacks + subTable.find('.button-uninstall').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + "{% url 'stock-item-uninstall' %}", + { + data: { + 'items[]': [pk], + }, + success: function() { + // Refresh entire table! + table.bootstrapTable('refresh'); + } + } + ); + }); + } + }); + }, columns: [ { checkbox: true, @@ -861,7 +964,7 @@ function loadInstalledInTable(table, options) { sortable: true, formatter: function(value, row, index, field) { - var url = `/stock/item/${row.pk}/`; + var url = `/part/${row.sub_part}/`; var thumb = row.sub_part_detail.thumbnail; var name = row.sub_part_detail.full_name; @@ -877,9 +980,11 @@ function loadInstalledInTable(table, options) { formatter: function(value, row, index, field) { // Construct a progress showing how many items have been installed - var installed = row.installed || 0; + var installed = row.installed_count || 0; var required = row.quantity || 0; + required *= options.quantity; + var progress = makeProgressBar(installed, required, { id: row.sub_part.pk, }); @@ -891,7 +996,7 @@ function loadInstalledInTable(table, options) { field: 'actions', switchable: false, formatter: function(value, row) { - var pk = row.sub_part.pk; + var pk = row.sub_part; var html = `<div class='btn-group float-right' role='group'>`; @@ -904,8 +1009,63 @@ function loadInstalledInTable(table, options) { } ], onLoadSuccess: function() { - console.log('data loaded!'); - } + // Grab a list of parts which are actually installed in this stock item + + inventreeGet( + "{% url 'api-stock-list' %}", + { + installed_in: options.stock_item, + }, + { + success: function(stock_items) { + + var table_data = table.bootstrapTable('getData'); + + stock_items.forEach(function(item) { + + var match = false; + + for (var idx = 0; idx < table_data.length; idx++) { + + var row = table_data[idx]; + + // Check each row in the table to see if this stock item matches + table_data.forEach(function(row) { + + // Match on "sub_part" + if (row.sub_part == item.part) { + + // First time? + if (row.installed_count == null) { + row.installed_count = 0; + row.installed_items = []; + } + + row.installed_count += item.quantity; + row.installed_items.push(item); + + // Push the row back into the table + table.bootstrapTable('updateRow', idx, row, true); + + match = true; + } + + }); + + if (match) { + break; + } + } + }); + + // Update button callback links + updateCallbacks(); + } + } + ); + + updateCallbacks(); + }, } ); } \ No newline at end of file From 824ce6778fed69face76d505be0ffe614153714a Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 23:33:20 +1100 Subject: [PATCH 33/77] Progress bar tweaks - If no maximum value supplied, just show the value (and fill to 100% width) --- .../static/script/inventree/inventree.js | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index 45565f1d6a..263e28fc01 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -105,9 +105,14 @@ function makeProgressBar(value, maximum, opts) { var options = opts || {}; value = parseFloat(value); - maximum = parseFloat(maximum); - var percent = parseInt(value / maximum * 100); + var percent = 100; + + // Prevent div-by-zero or null value + if (maximum && maximum > 0) { + maximum = parseFloat(maximum); + percent = parseInt(value / maximum * 100); + } if (percent > 100) { percent = 100; @@ -115,18 +120,28 @@ function makeProgressBar(value, maximum, opts) { var extraclass = ''; - if (value > maximum) { + if (maximum) { + // TODO - Special color? + } + else if (value > maximum) { extraclass='progress-bar-over'; } else if (value < maximum) { extraclass = 'progress-bar-under'; } + var text = value; + + if (maximum) { + text += ' / '; + text += maximum; + } + var id = options.id || 'progress-bar'; return ` <div id='${id}' class='progress'> <div class='progress-bar ${extraclass}' role='progressbar' aria-valuenow='${percent}' aria-valuemin='0' aria-valuemax='100' style='width:${percent}%'></div> - <div class='progress-value'>${value} / ${maximum}</div> + <div class='progress-value'>${text}</div> </div> `; } From b9291c6705a464ae7b0a1a3327bf7b4db4841dd5 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 23:33:43 +1100 Subject: [PATCH 34/77] Improve transaction note recording for the StockItem model --- InvenTree/stock/models.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 964ab43e8a..b78b0e9410 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -625,11 +625,19 @@ class StockItem(MPTTModel): stock_item.belongs_to = self stock_item.save() - # Add a transaction note! + # Add a transaction note to the other item stock_item.addTransactionNote( _('Installed into stock item') + ' ' + str(self.pk), user, - notes=notes + notes=notes, + url=self.get_absolute_url() + ) + + # Add a transaction note to this item + self.addTransactionNote( + _('Installed stock item') + ' ' + str(stock_item.pk), + user, notes=notes, + url=stock_item.get_absolute_url() ) @transaction.atomic @@ -649,16 +657,31 @@ class StockItem(MPTTModel): # TODO - Are there any other checks that need to be performed at this stage? + # Add a transaction note to the parent item + self.belongs_to.addTransactionNote( + _("Uninstalled stock item") + ' ' + str(self.pk), + user, + notes=notes, + url=self.get_absolute_url(), + ) + + # Mark this stock item as *not* belonging to anyone self.belongs_to = None self.location = location self.save() + if location: + url = location.get_absolute_url() + else: + url = '' + # Add a transaction note! self.addTransactionNote( _('Uninstalled into location') + ' ' + str(location), user, - notes=notes + notes=notes, + url=url ) @property From 46f459b4c74462e554395225068df13db1934261 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 23:34:02 +1100 Subject: [PATCH 35/77] Better display of stock table --- InvenTree/templates/js/stock.html | 47 +++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html index 330be924e5..b3ea0f2baa 100644 --- a/InvenTree/templates/js/stock.html +++ b/InvenTree/templates/js/stock.html @@ -470,10 +470,16 @@ function loadStockTable(table, options) { if (row.customer) { html += `<span class='fas fa-user-tie label-right' title='{% trans "Stock item has been assigned to customer" %}'></span>`; - } else if (row.build_order) { - html += `<span class='fas fa-tools label-right' title='{% trans "Stock item was assigned to a build order" %}'></span>`; - } else if (row.sales_order) { - html += `<span class='fas fa-dollar-sign label-right' title='{% trans "Stock item was assigned to a sales order" %}'></span>`; + } else { + if (row.build_order) { + html += `<span class='fas fa-tools label-right' title='{% trans "Stock item was assigned to a build order" %}'></span>`; + } else if (row.sales_order) { + html += `<span class='fas fa-dollar-sign label-right' title='{% trans "Stock item was assigned to a sales order" %}'></span>`; + } + } + + if (row.belongs_to) { + html += `<span class='fas fa-box label-right' title='{% trans "Stock item has been installed in another item" %}'></span>`; } // Special stock status codes @@ -520,6 +526,9 @@ function loadStockTable(table, options) { } else if (row.customer) { var text = "{% trans "Shipped to customer" %}"; return renderLink(text, `/company/${row.customer}/assigned-stock/`); + } else if (row.sales_order) { + var text = `{% trans "Assigned to sales order" %}`; + return renderLink(text, `/order/sales-order/${row.sales_order}/`); } else if (value) { return renderLink(value, `/stock/location/${row.location}/`); @@ -844,6 +853,10 @@ function loadInstalledInTable(table, options) { data: { part: pk, }, + success: function() { + // Refresh entire table! + table.bootstrapTable('refresh'); + } } ); }); @@ -895,7 +908,7 @@ function loadInstalledInTable(table, options) { html += `{% trans "Quantity" %}: ${subrow.quantity}`; } - return html; + return renderLink(html, `/stock/item/${subrow.pk}/`); }, }, { @@ -969,6 +982,10 @@ function loadInstalledInTable(table, options) { var name = row.sub_part_detail.full_name; html = imageHoverIcon(thumb) + renderLink(name, url); + + if (row.not_in_bom) { + html = `<i>${html}</i>` + } return html; } @@ -1015,6 +1032,7 @@ function loadInstalledInTable(table, options) { "{% url 'api-stock-list' %}", { installed_in: options.stock_item, + part_detail: true, }, { success: function(stock_items) { @@ -1056,6 +1074,25 @@ function loadInstalledInTable(table, options) { break; } } + + if (!match) { + // The stock item did *not* match any items in the BOM! + // Add a new row to the table... + + console.log("Found an unmatched part! " + item.pk + " -> " + item.part); + + // Contruct a new "row" to add to the table + var new_row = { + sub_part: item.part, + sub_part_detail: item.part_detail, + not_in_bom: true, + installed_count: item.quantity, + installed_items: [item], + }; + + table.bootstrapTable('append', [new_row]); + + } }); // Update button callback links From 42a75a82382b21f0f8137385fc45ad092fd22e4a Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 23:45:52 +1100 Subject: [PATCH 36/77] Add hidden input to the InstallStockForm form - keeps track of "part" object - so we can filter the stock_items queryset if the form validation fails - Is there a more djangonic way of doing this?? --- InvenTree/stock/forms.py | 8 ++++++++ InvenTree/stock/views.py | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 729831a9ec..85a520fed5 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -19,6 +19,8 @@ from InvenTree.fields import RoundingDecimalFormField from report.models import TestReport +from part.models import Part + from .models import StockLocation, StockItem, StockItemTracking from .models import StockItemAttachment from .models import StockItemTestResult @@ -278,6 +280,11 @@ class InstallStockForm(HelperForm): Form for manually installing a stock item into another stock item """ + part = forms.ModelChoiceField( + queryset=Part.objects.all(), + widget=forms.HiddenInput + ) + stock_item = forms.ModelChoiceField( required=True, queryset=StockItem.objects.filter(StockItem.IN_STOCK_FILTER), @@ -302,6 +309,7 @@ class InstallStockForm(HelperForm): class Meta: model = StockItem fields = [ + 'part', 'stock_item', 'quantity_to_install', 'notes', diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index fe2ffbe7c6..860ef0c74b 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -699,6 +699,8 @@ class StockItemInstall(AjaxUpdateView): form_class = StockForms.InstallStockForm ajax_form_title = _('Install Stock Item') + part = None + def get_stock_items(self): """ Return a list of stock items suitable for displaying to the user. @@ -713,14 +715,19 @@ class StockItemInstall(AjaxUpdateView): items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER) # Filter by Part association - try: - part = self.request.GET.get('part', None) - if part is not None: - part = Part.objects.get(pk=part) - items = items.filter(part=part) + # Look at GET params + part_id = self.request.GET.get('part', None) + + if part_id is None: + # Look at POST params + part_id = self.request.POST.get('part', None) + + try: + self.part = Part.objects.get(pk=part_id) + items = items.filter(part=self.part) except (ValueError, Part.DoesNotExist): - pass + self.part = None return items @@ -735,6 +742,9 @@ class StockItemInstall(AjaxUpdateView): item = items.first() initials['stock_item'] = item.pk initials['quantity_to_install'] = item.quantity + + if self.part: + initials['part'] = self.part return initials From 852da6d696feac6d3241aaf8741280fbd4cc1194 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 23:48:15 +1100 Subject: [PATCH 37/77] Fix form validation --- InvenTree/stock/forms.py | 8 ++++---- InvenTree/templates/js/stock.html | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 85a520fed5..2502fc1869 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -282,7 +282,7 @@ class InstallStockForm(HelperForm): part = forms.ModelChoiceField( queryset=Part.objects.all(), - widget=forms.HiddenInput + widget=forms.HiddenInput() ) stock_item = forms.ModelChoiceField( @@ -321,10 +321,10 @@ class InstallStockForm(HelperForm): print("Data:", data) - stock_item = data['stock_item'] - quantity = data['quantity_to_install'] + stock_item = data.get('stock_item', None) + quantity = data.get('quantity_to_install', None) - if quantity > stock_item.quantity: + if stock_item and quantity and quantity > stock_item.quantity: raise ValidationError({'quantity_to_install': _('Must not exceed available quantity')}) return data diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html index b3ea0f2baa..0ebedbfb6e 100644 --- a/InvenTree/templates/js/stock.html +++ b/InvenTree/templates/js/stock.html @@ -1078,8 +1078,6 @@ function loadInstalledInTable(table, options) { if (!match) { // The stock item did *not* match any items in the BOM! // Add a new row to the table... - - console.log("Found an unmatched part! " + item.pk + " -> " + item.part); // Contruct a new "row" to add to the table var new_row = { From 3fe08862071824624d7b69dcaf32f077467e683f Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 4 Oct 2020 23:49:01 +1100 Subject: [PATCH 38/77] Remove a debug statement --- InvenTree/stock/forms.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 2502fc1869..548a03ae90 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -319,8 +319,6 @@ class InstallStockForm(HelperForm): data = super().clean() - print("Data:", data) - stock_item = data.get('stock_item', None) quantity = data.get('quantity_to_install', None) From 62734c4b7298fc1da248d644890ca225c64728a1 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 00:01:01 +1100 Subject: [PATCH 39/77] Add a custom template for the install item form --- .../stock/templates/stock/item_install.html | 17 +++++++++++++++++ .../stock/templates/stock/item_serialize.html | 6 ++++-- InvenTree/stock/views.py | 1 + InvenTree/templates/js/stock.html | 7 ++++--- 4 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 InvenTree/stock/templates/stock/item_install.html diff --git a/InvenTree/stock/templates/stock/item_install.html b/InvenTree/stock/templates/stock/item_install.html new file mode 100644 index 0000000000..04798972d2 --- /dev/null +++ b/InvenTree/stock/templates/stock/item_install.html @@ -0,0 +1,17 @@ +{% extends "modal_form.html" %} +{% load i18n %} + +{% block pre_form_content %} + +<p> + {% trans "Install another StockItem into this item." %} +</p> +<p> + {% trans "Stock items can only be installed if they meet the following criteria" %}: + + <ul> + <li>{% trans "The StockItem links to a Part which is in the BOM for this StockItem" %}</li> + <li>{% trans "The StockItem is currently in stock" %}</li> + </ul> +</p> +{% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/item_serialize.html b/InvenTree/stock/templates/stock/item_serialize.html index bb0054cca2..0f70647e38 100644 --- a/InvenTree/stock/templates/stock/item_serialize.html +++ b/InvenTree/stock/templates/stock/item_serialize.html @@ -1,6 +1,8 @@ {% extends "modal_form.html" %} +{% load i18n %} {% block pre_form_content %} -Create serialized items from this stock item.<br> -Select quantity to serialize, and unique serial numbers. +{% trans "Create serialized items from this stock item." %} +<br> +{% trans "Select quantity to serialize, and unique serial numbers." %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 860ef0c74b..9d078bf702 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -698,6 +698,7 @@ class StockItemInstall(AjaxUpdateView): model = StockItem form_class = StockForms.InstallStockForm ajax_form_title = _('Install Stock Item') + ajax_template_name = "stock/item_install.html" part = None diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html index 0ebedbfb6e..eb5e4adbf7 100644 --- a/InvenTree/templates/js/stock.html +++ b/InvenTree/templates/js/stock.html @@ -899,9 +899,6 @@ function loadInstalledInTable(table, options) { var pk = subrow.pk; var html = ''; - html += row.sub_part_detail.full_name; - html += " | "; - if (subrow.serial && subrow.quantity == 1) { html += `{% trans "Serial" %}: ${subrow.serial}`; } else { @@ -918,6 +915,10 @@ function loadInstalledInTable(table, options) { return stockStatusDisplay(value); } }, + { + field: 'batch', + title: '{% trans "Batch" %}', + }, { field: 'actions', title: '', From ee28b4eea55e6f354531f125cb74af456ee320ff Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 00:12:42 +1100 Subject: [PATCH 40/77] Add "is_building" field to StockItem model - This will be set to TRUE until a stock item has been completed --- .../migrations/0052_stockitem_is_building.py | 18 ++++++++++++++++++ InvenTree/stock/models.py | 5 +++++ 2 files changed, 23 insertions(+) create mode 100644 InvenTree/stock/migrations/0052_stockitem_is_building.py diff --git a/InvenTree/stock/migrations/0052_stockitem_is_building.py b/InvenTree/stock/migrations/0052_stockitem_is_building.py new file mode 100644 index 0000000000..46847992cc --- /dev/null +++ b/InvenTree/stock/migrations/0052_stockitem_is_building.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-10-04 13:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0051_auto_20200928_0928'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='is_building', + field=models.BooleanField(default=False), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index b78b0e9410..75d38280d0 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -130,6 +130,7 @@ class StockItem(MPTTModel): status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus) notes: Extra notes field build: Link to a Build (if this stock item was created from a build) + is_building: Boolean field indicating if this stock item is currently being built purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) infinite: If True this StockItem can never be exhausted sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder) @@ -389,6 +390,10 @@ class StockItem(MPTTModel): related_name='build_outputs', ) + is_building = models.BooleanField( + default=False, + ) + purchase_order = models.ForeignKey( 'order.PurchaseOrder', on_delete=models.SET_NULL, From 26d113e8adc5681d297cff7d434e0331dc8e16c1 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 00:14:04 +1100 Subject: [PATCH 41/77] Update IN_STOCK_FILTER to reject stock items which have is_building set to True --- InvenTree/stock/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 75d38280d0..3277137a25 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -143,6 +143,7 @@ class StockItem(MPTTModel): build_order=None, belongs_to=None, customer=None, + is_building=False, status__in=StockStatus.AVAILABLE_CODES ) From fe3a72c6cc878787f1bc427d700173d4527b990a Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 00:29:06 +1100 Subject: [PATCH 42/77] Add some unit testing --- InvenTree/part/models.py | 9 +++++---- InvenTree/stock/models.py | 4 ++++ InvenTree/stock/tests.py | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 1400abd225..5f29c8cf80 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -784,12 +784,13 @@ class Part(MPTTModel): """ Return the current number of parts currently being built """ - quantity = self.active_builds.aggregate(quantity=Sum('quantity'))['quantity'] + stock_items = self.stock_items.filter(is_building=True) - if quantity is None: - quantity = 0 + query = stock_items.aggregate( + quantity=Coalesce(Sum('quantity'), Decimal(0)) + ) - return quantity + return query['quantity'] def build_order_allocations(self): """ diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 3277137a25..fbdc1654f3 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -721,6 +721,10 @@ class StockItem(MPTTModel): if self.customer is not None: return False + # Not 'in stock' if it is building + if self.is_building: + return False + # Not 'in stock' if the status code makes it unavailable if self.status in StockStatus.UNAVAILABLE_CODES: return False diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 23cf6b3d02..135cb48f8b 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -47,6 +47,30 @@ class StockTest(TestCase): Part.objects.rebuild() StockItem.objects.rebuild() + def test_is_building(self): + """ + Test that the is_building flag does not count towards stock. + """ + + part = Part.objects.get(pk=1) + + # Record the total stock count + n = part.total_stock + + StockItem.objects.create(part=part, quantity=5) + + # And there should be *no* items being build + self.assertEqual(part.quantity_being_built, 0) + + # Add some stock items which are "building" + for i in range(10): + item = StockItem.objects.create(part=part, quantity=10, is_building=True) + + # The "is_building" quantity should not be counted here + self.assertEqual(part.total_stock, n + 5) + + self.assertEqual(part.quantity_being_built, 100) + def test_loc_count(self): self.assertEqual(StockLocation.objects.count(), 7) From c1595396c44a683e65f554a247c8014a5b12d8e9 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 00:29:31 +1100 Subject: [PATCH 43/77] Unit testing: fix PEP issues --- InvenTree/stock/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 135cb48f8b..44b901e7b6 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -64,7 +64,7 @@ class StockTest(TestCase): # Add some stock items which are "building" for i in range(10): - item = StockItem.objects.create(part=part, quantity=10, is_building=True) + StockItem.objects.create(part=part, quantity=10, is_building=True) # The "is_building" quantity should not be counted here self.assertEqual(part.total_stock, n + 5) From 3ee7be1d5845760d41f690d298bc5c8ebd6f92e6 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 00:42:09 +1100 Subject: [PATCH 44/77] Add "optional" field to BomItem - Defaults to False - Indicates that the BomItem is "optional" for a build - Will be used in the future when calculating if a Build output is fully allocated! --- InvenTree/part/api.py | 16 +++++++++++++--- InvenTree/part/forms.py | 3 ++- .../part/migrations/0051_bomitem_optional.py | 18 ++++++++++++++++++ InvenTree/part/models.py | 3 +++ InvenTree/part/serializers.py | 1 + InvenTree/templates/js/bom.html | 4 ++++ 6 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 InvenTree/part/migrations/0051_bomitem_optional.py diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 9a86bb98d5..6834503466 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -765,20 +765,30 @@ class BomList(generics.ListCreateAPIView): queryset = super().filter_queryset(queryset) + params = self.request.query_params + + # Filter by "optional" status? + optional = params.get('optional', None) + + if optional is not None: + optional = str2bool(optional) + + queryset = queryset.filter(optional=optional) + # Filter by part? - part = self.request.query_params.get('part', None) + part = params.get('part', None) if part is not None: queryset = queryset.filter(part=part) # Filter by sub-part? - sub_part = self.request.query_params.get('sub_part', None) + sub_part = params.get('sub_part', None) if sub_part is not None: queryset = queryset.filter(sub_part=sub_part) # Filter by "trackable" status of the sub-part - trackable = self.request.query_params.get('trackable', None) + trackable = params.get('trackable', None) if trackable is not None: trackable = str2bool(trackable) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 0a15d598bd..c64bbb8362 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -231,7 +231,8 @@ class EditBomItemForm(HelperForm): 'quantity', 'reference', 'overage', - 'note' + 'note', + 'optional', ] # Prevent editing of the part associated with this BomItem diff --git a/InvenTree/part/migrations/0051_bomitem_optional.py b/InvenTree/part/migrations/0051_bomitem_optional.py new file mode 100644 index 0000000000..04920b3c10 --- /dev/null +++ b/InvenTree/part/migrations/0051_bomitem_optional.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-10-04 13:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0050_auto_20200917_2315'), + ] + + operations = [ + migrations.AddField( + model_name='bomitem', + name='optional', + field=models.BooleanField(default=False, help_text='This BOM item is optional'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 5f29c8cf80..d427409c71 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1500,6 +1500,7 @@ class BomItem(models.Model): part: Link to the parent part (the part that will be produced) sub_part: Link to the child part (the part that will be consumed) quantity: Number of 'sub_parts' consumed to produce one 'part' + optional: Boolean field describing if this BomItem is optional reference: BOM reference field (e.g. part designators) overage: Estimated losses for a Build. Can be expressed as absolute value (e.g. '7') or a percentage (e.g. '2%') note: Note field for this BOM item @@ -1533,6 +1534,8 @@ class BomItem(models.Model): # Quantity required quantity = models.DecimalField(default=1.0, max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], help_text=_('BOM quantity for this BOM item')) + optional = models.BooleanField(default=False, help_text=_("This BOM item is optional")) + overage = models.CharField(max_length=24, blank=True, validators=[validators.validate_overage], help_text=_('Estimated build wastage quantity (absolute or percentage)') ) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 7c73e9f98b..847216e957 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -403,6 +403,7 @@ class BomItemSerializer(InvenTreeModelSerializer): 'quantity', 'reference', 'price_range', + 'optional', 'overage', 'note', 'validated', diff --git a/InvenTree/templates/js/bom.html b/InvenTree/templates/js/bom.html index db5eafa0c1..b804453ca2 100644 --- a/InvenTree/templates/js/bom.html +++ b/InvenTree/templates/js/bom.html @@ -169,6 +169,10 @@ function loadBomTable(table, options) { // Let's make it a bit more pretty text = parseFloat(text); + if (row.optional) { + text += " ({% trans "Optional" %})"; + } + if (row.overage) { text += "<small> (+" + row.overage + ") </small>"; } From 48e050d3171c16f16d92feae7ff63d08b960818e Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 00:49:00 +1100 Subject: [PATCH 45/77] Add some more unit tests and validation code for the StockItem model - Ensure that the build part matches the stockitem part! --- InvenTree/stock/models.py | 14 ++++++++++++++ InvenTree/stock/tests.py | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index fbdc1654f3..1535ded420 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -275,11 +275,25 @@ class StockItem(MPTTModel): # TODO - Find a test than can be perfomed... pass + # Ensure that the item cannot be assigned to itself if self.belongs_to and self.belongs_to.pk == self.pk: raise ValidationError({ 'belongs_to': _('Item cannot belong to itself') }) + # If the item is marked as "is_building", it must point to a build! + if self.is_building and not self.build: + raise ValidationError({ + 'build': _("Item must have a build reference if is_building=True") + }) + + # If the item points to a build, check that the Part references match + if self.build: + if not self.part == self.build.part: + raise ValidationError({ + 'build': _("Build reference does not point to the same part object") + }) + def get_absolute_url(self): return reverse('stock-item-detail', kwargs={'pk': self.id}) diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 44b901e7b6..34fe8877f8 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -7,7 +7,9 @@ import datetime from .models import StockLocation, StockItem, StockItemTracking from .models import StockItemTestResult + from part.models import Part +from build.models import Build class StockTest(TestCase): @@ -62,9 +64,14 @@ class StockTest(TestCase): # And there should be *no* items being build self.assertEqual(part.quantity_being_built, 0) + build = Build.objects.create(part=part, title='A test build', quantity=1) + # Add some stock items which are "building" for i in range(10): - StockItem.objects.create(part=part, quantity=10, is_building=True) + StockItem.objects.create( + part=part, build=build, + quantity=10, is_building=True + ) # The "is_building" quantity should not be counted here self.assertEqual(part.total_stock, n + 5) From 13cd8624b2a5a7bdec5c09b810126fb7188fff6b Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 01:01:56 +1100 Subject: [PATCH 46/77] Fix permissions --- InvenTree/templates/navbar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 57a902b755..2224d85bc4 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -21,7 +21,7 @@ {% if perms.stock.view_stockitem or perms.part.view_stocklocation %} <li><a href="{% url 'stock-index' %}"><span class='fas fa-boxes icon-header'></span>{% trans "Stock" %}</a></li> {% endif %} - {% if perms.build %} + {% if perms.build.view_build %} <li><a href="{% url 'build-index' %}"><span class='fas fa-tools icon-header'></span>{% trans "Build" %}</a></li> {% endif %} {% if perms.order.view_purchaseorder %} From bce7eb1aad16e36300cdf94c7f63760fc9696c9f Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 01:02:36 +1100 Subject: [PATCH 47/77] update translation files --- InvenTree/locale/de/LC_MESSAGES/django.mo | Bin 49527 -> 49430 bytes InvenTree/locale/de/LC_MESSAGES/django.po | 1089 ++++++++++++--------- InvenTree/locale/en/LC_MESSAGES/django.po | 1041 +++++++++++--------- InvenTree/locale/es/LC_MESSAGES/django.po | 1041 +++++++++++--------- 4 files changed, 1748 insertions(+), 1423 deletions(-) diff --git a/InvenTree/locale/de/LC_MESSAGES/django.mo b/InvenTree/locale/de/LC_MESSAGES/django.mo index 6c5d41663c4d432a6b181ba9b0917325f50dcb2c..6391f0ae5a5d9db6fe8c2b5bbd59d2a7fd8e05ab 100644 GIT binary patch delta 15503 zcmYk?1$39yAII^B+6D{8fNk`E(Tu_9ZlzngyQEVlk04zNBPFCpN=^`@QBV+2Fo3@p zm`Di<3JUmte|En+hv%H@>)!8;=hpL^f6xE#sTr0|&ES6<k#UK~b3VQ2<;7`{p7&7( z&%0e(QO^si;dxmxGX`KT48a%-!#E7Z8mM+nFduft+&IDFNf=Fe1LnoUn9cKi?<$$> z1a7&2_aA1aoUNwkg<~F6$I=*x4N((lj+wACs(nw?1O}rf_6kPfY~)4WI*X^6Cowbc z_b!rA$FH$2-ojiMSIhY#mZjVa<8Tqu#5->BXJ&kD&x<GC6IK5vY9bdf3x0z^c*DGp z5k7%`$OL0(9k=3Ks4Xptd9jN55^ANRP!mo>PRg5yioc0^;aeDiTTv@Nj9SnI%#Sxv z?E_h@uS_;F+R8krnHIztERE{W95v8TD^EtXUyYjR2Gj!fm`AWb<uj<GDqGLBtBIOm zQ&hZjJ@#K62N2K<N2AVqJ{H1FsQNRg4qu`s@&oE@AE53|NPV}pF{n#g0<|+`Q46eK z*23bH8)11IS)ctcOeUE?DLjLE(L*zR16R(9+S0tJ0psxntcum}H7tXNto|nItRG+w ze2Tiv5e?l=6h?g=<$N*~$TUN(YznGl66&_ELhZmtb2n-NhpqlJ>MmSB9mTg+zKvSQ z6Vwi6X0|dPYW#AjqwuR)rZH;Zj%IJvQ4B>*WIE==`KXz1u==Bzhw^1CfPY$j_7~j| zl|g<|ycbaYyP?J%?ex8QWYjSkbrh#C5Pw5;xR2Vxz(#H-@}dSTj*8bt?OZ#n?~j_$ zDAdj@K&^a@mG`2?J&F1C{a>-b->5SVZS1xt233wn4N%W)kGgz=Q7@Q^8gLQn5^q7Z zKZ#oDSEzQsTlqg!yXYn?g!g+1WE5zO>d*(Z1BsXgr=eE(25R7SsLQkuwG*dNEB*#` z2OeYUjx}}T<wdoth&uCT7=&HW*9!ZR$%o@HGcG|*;BC|kH)BRTgnIFD^CFg_{DT?R z%<V)q3?SYLHSrFp9T<cmI1x3G8O_*#RV*Q(fs;`)-H%$qdDPbafchbNgdrH!+#O9U zs$3cMq6QXkkLouFwKG0yyp^c;?MAgf-kkl{i!WK=N7UIqM0E&l;ePWAp?0bjs$(Bi zyRqhc45pln`a1Sl{G9od#h;*dII5-F;etMyFajm8I99<z*b8HE8kWJWR=$F2{}eTW zkXCM`v8W@ehPp$wF)MaNo%KKr#gV3uT97}VjJ9yCRqQ~$;E**qi@F2fp#B)$N9{~l zYqxWSQS}w9+z7Sxolp}PZuLIuk|&{FyaSn#?;R(jh8IvT{uZ;~9n?UNP#wbBxQP}- zZE+dQgpIA-5_M?@qgFZ!`7QEZ!%FxO>TW$l{odqk%O5y>{}svT?0TbqfQF%N^&%{S zN3kmYgu2!F+qo?+f_hOc%!bWSN6;NLULVxX4o5945!HV#>JBVYS>OLEGCHFTm;v8G zZS`){OwXfs=nksgQ#0Tt_f=#?eZFN-ccvNU#F3Z-XIgv}YC<Pa6Tg7IGS|qc;Z4*Q z{fGM8BHKGFVg%)Os52aZkvIjzak<5}qAukJs2%wlHL*LW7YBD>w=f!uVTTUvzZ&=! zSdE&<DbyDJ54B~#pmyXAMq!qYZUu!<cc3h4iz}ff*buc-JuE&DHG%P{ot%c*adAiX zUo+Z5Kxg->Roq5R#Ovg?G$-bySPkQ_73u|(F?EYkJFyD2qdQO&JA_)m3Do!>qt5=k zdD$nUGrx|S;m;V2f1m~m?(DWY7phzq^@~;u^*hlS_2O4iU(Eu{g)30wq@Z4W4mJKo z)DC=$+F}2$Rs3THc5wsdK+UuW>c#P>71u=Fg%?l*bwX|ZAk+&cpw51})i1;JlsBVx zY8$HmF=Ra7yF^B3@il73zoTXr)YYAN4pfJ7sCEs^jusz^>gS{G#&Rr(yD<_kp(gkn zY66ce9@<U)IR98O`pjyg&bS+DpwXBQ=URL#YQSTt`mazEx{KQ4^xfT=2cjkvY8J&> zlxw4QVlrwcr(ihm_vVn%FV{-crP+Ww%dM!Lc-K5`@pD$bj5>lJQCt5fYU{lou3rRd zocySEB~b0ETKx;?>vpywQx-d6UYvuvT$@oH&Z4&PGU_OPK@IrW%-qxSic-#ndSM+@ zzow`KbTs>-CO87)aB5HXUt71EKmzVV-R2vZ`U2GD%iPNi7=^l&v8Wf7LA9%D^^MK; zR^JD8WW!M7Oh+AE5^BQRd--lAmk8t|;PrODc=@pe#hMs{1F#^@LJhPH^_d+&P52V( za{Y+8@e%58NA^DaeZi_&28W{-um&~$7N3l^_7WDr>sS)g^>s5Wjj@#5qUtB0Ubq}} zSGHPw7wQguYVpgct^Wpf#J{2X-LrDKey+bCL`EIMP`5b})v+XMVwF%UYKYp(o~V_M zM{WH)EQG62XMPNIyU(Gv_)9E{Pp}xq_1B+W&wB~!=X+Dh=yOR%ZRt_0j8{;XF?@jA z@;KDxsgC)vIci6SqFy);wY5u7?cPQm*&Zu@j#}8ysQ28*5Pko_1Kr>KXw(a9pa$%O z+OpnO9&ILK2IA8(5ND%Sz7X|-HK<Fr4Yi{uQ4{(K_4{%I)&3b)=lx#rAonjEFJeZ@ zqcH#{ppL+|@&eS%m!JmRg&Ob#>L@N_dHfZ1N%IVLe=AC$+BL)?*aa)$6!cYbfJ`2| zfIaXgYUWLbxEFRuefJYkE1ZkE1Itif%{x{;Yvu1y6MBl8Xy{O90n`M`p%&V2DEqIO z4kn-(O+Za#7wU{Z!4h~AHDHdH-HT&U16RdZY>K+1Bd`|E#UMP3n&{^kk2g>|8a>SY zeiR$V{%0YOKp+%rqRzA_Ho&&17cEC!(ru^-et??rcNl_^!`%c6qjt1B>h3hgFzk#P zcM$45BT@Z)pG;OV^H396g*yAqSQvMrR(J_B;oqnMo}yM3Ji_&hK^;*k)Qf9bxg{2$ z+#7>&D)L+BEkI4ozeh&j^?#TZBS*T5qNo{GL~U(7RDCy$$Cpu`*J{*6kD=QCX!Z9{ zI~6m^-K83+iMK+v>yFuYzc+-8&gxZbFvDDo+RF8)l^#G{%JZn1U&jc%XXOl|-49L# z>QYuky|4r7tLcZDkdK*hB?jsHPbQ;=JFz$(My=>)Ou)=z7z}G*X&i^T8yirU^ad8i zz_IT4qYUx|dQDLi*@v3I=cpgHI~a#W#*uix*Op8voQVA0@RG4H290-T-Uc<}j;NjL zjX7`#YND^9?#@OG$6cs)$B`}e&Y<qj*Qg)3>!|+EF!g``XP@A<x+rQ(YoI!|L=D&r zwZc)TTf71@;yTn>ZbG$BLEV*8s26^Zn%FO>3Eju+_z!BFu!-!yGLaMArOJ;gw?M6| zJ?fIZf*EiX_D|<|Yf+amaFTmb6lx;*PzxxDT47C$#x|G>hoB}n-QsH}vHv9r93)Tz zf5My?mgv5USkwU3Q8RC64n*CRMAU?nQT=wHR{jBM<!4Yk^M%#_f?CiM)Oh**SKVz+ zKn+kA)v*JrV{dGUV=xbXh?>y%SRWswE@92dTsZ8CRd5T|#p_rd3$xG`*aLN!cB0<n zAF#j&7C4WZ*%j28-9~NcLyW|r*W3yUm~~MD^+9d%Fw_stEYt!PV*%WR+OgB9BlsQJ zao@`_g+B-cDxhZ8A2r|z)R|ARxR3fJoQ(x>C+bC?p?2mPs{JEW|0kGwN2a=R6!s!s z2z6JcVX(gch1OuTxdk<m6x6^+Fbc0?e!P#`nef-$z`0OANU^AumPZ|BThveaAk^0~ z!pdV&6G)W2-<wUQGA_lccmYdc&@}hohAW_cpa!7^9&gS^9nA*Rg!f}pJdSlRWV-uv z+Z^@6DX4Y}P)E5OeVx%RG8*Uv=EW~izfkv4^#x|Qm6SsbPz5z$J=77jwE8ZXiE?k$ zga@MfkF)YD)X^+O9nHEK?7tfBArOGaP#sRAR(i$acTpW5Vo~&Fx)&Ei9Z7lAirQFt z5ay#i3ya_;i=V?7%C}ILJY*L8uQQ98<p!#Qg(&wx?Z8x2{ca4z&rlP-g4%&!%s)}L z+MDerk_Ytzlz>|4ix`6)EIt7>;rTupZT&{nir+&W!Fh|{L=Es1^-~-=$F<LodSN-# zi<+T+7kZ%veg(Ay3sF0?%jz$n#{U)dUO(ep_u?qjEswKuL)1WBP#s2L5Waz0`Et|% z$*8S7V4g!w?0eLU|3$UWJ<p9-4E0sja{69pGWvYRp)SvKE6+u({4ET?Z5WJuP%A!y zYJUlJXRe?oaueh68R`hj%y;d(q82a`HSQ~zQ{Vp_G9d)kq9(EfwX(ygfj>nJd>yqT z56zGTZi@?}`qf1pVJp;8^+!!~yv66C##@iY@Li1N{oXY)>hKUXvw$~T!$`9@YC_de zXWSgiV1HD<C04%)^}>UwBfEfF*e$Dngkh9}-*h{f8&kjkqGU2tQ3Z9&>!BJpL46IK zurMZ~F4bC$$D^o~+{JK=T<9iP40Q+UU>59xnpl6#hLbS$_Ag}rb?G(|(8>;@w(v{T zfVWUv|HKSm<l4oWHBp~ud(?`@p~g!>jk6si@C0i7FHkSOgX)(tiTy7@CN|0a^=g6Y zFc7ohB+QQUQ3J0-bv%grYj+j3Q-7i+kbbedlm$@bBB)DO54E7CSPQ#i1zhQq(bj&3 z8t{Qtge`F$>SGM?c31<)qB`!yviK|NtB79eCK!vMlxtuPY=ZhqdZ60%!>l+Gv!g$S zj4sI%)J)f-?!b041@*liLaqEV>Q4NP+L7GL+}%k)^{avUiEe<!aR^qy#i+Y;9<?*K zupsaEf|k22EQy+7Rn!U^qGs3@bK(Gu#aB@~wjLw#wD}E2QGS5h>W~#~2lAof38*7& zf?7yd4Ab|YNTwlyxv0Bv5jE3)F#&VE<zCbbHK9JJTRk3i`=??q+=lw0`q1h>L5=ej zYR4X+CJ?pK9YG1q!TY_cWOV6TpkB}mtK(SAh<h+M?l&)@2E2{hfvl_Czd;p69bre* zj`c$A#CX(>&O&|WOU<?DtHU-jnpq0!a-6~p_$6wkS5Yte$;$s?5y}CpUA#2LP_B=< zOMOw7ZzO8G8JHKBq9&GtTG*M@?7z0|0s+nR2I_^6QFkHZ8h1CsQS}8-1C}-GpxU>x zau?J}hoBZT5!G)l>cy+9elKbQ$Jem`dB~h1pc!6At>77I#ldg8Gc1I9aUG1oR;U+@ zwDRkyg=|FiJBs=7OVs!eP?tA+t!q~iwL>j^GJFPJUsT5#s1B=9XM7N&@fvD^4^f|0 z#5%X4>ZrTX8uh}VsEN!)efP<j`Z=S<NkNTs4%?ys8=3TEYOQydr!HzDO|0AtwZ(%` zTRRrDm9L}Ptw8+(?ndp<G4oSY`|nUMzGLx#4X%A8ax}hIgp3+iLTzaS)Dg7DD%b~0 z;wse2KSJH&d#JDFA5_1vjm{X<gc7hU)<v}+g<43W)z8G#|Ngg>j6S~&sPBJ2X2kEY zKK_jQN=hWV%ankcaDB5iYTzDLKiJ}vP!pMrTF`RTrA|iuAnj4TPv(pTu3$Xndsq%* zHo0HAHmIM}aaa*oqbB+}md2;3tuMLR-GOSTqv?ct(Fk)EY6sV113ZI%RWe~)+)Nvx zCeQ&j;7rt|T84V@ek_Hzu`Wh$b(gm@>NY20CR~Vxa2aZ6-p4>Zg*vkHm>I8c<@~iZ zw+IB`Bh&!twz-ahsM{TZ>JW$8!pf)#)<<n|ThtxsjT&zZ>MNRpdhu!uz+}`<`a9-# z+t`0yl8Eij`luC;F_TcYcQ4k&PcRlUzvCuQ26cC8VLoha4o6LRK5AjhPzza$Meq=6 zC$9Tsw1S5ihZ%Rcl_X#R$_-E*`l1G$gZjP~qx$Vcz2FFDz|$ClXHhS>j9Ks&^0|3` zp>{5Cr<=GRMMe!vqB_<>-TJ0j9N$2__yFo?PGT5-j2ieds@)H$ae{Zb--iNbMXXG` zDQ3mjQ9F_3^u3*Aw54ZIE4_<4f*iZuB`S~oDYr-c@Ek$ae~!A{e_&S3vd7)}+*qD+ zIn++|LyhaB`X{5ljzd^jfB!F$sZHQ7)JiMubvsc9n^JC$I@=Xk5Z}Xccoj8ZaEcqS zAnLAEL0!Jqs3UwC^+PrvL-90fVqaia-tXNdlMS1`>n6|*^*Ied9Z90aXJaeMNvIe9 zgnH3Gs2z#i=l-K~Mbw=biaM&Xs2!SS<;AG?Z9re2$38Ne*%9*-jG%lKgYh=%^Ld2D z@ISNIemB9Es26obO=t+J-%QMjNthkCnnzH7gf8y4@Bc1=W(594&Aicju0d<m0K-rd zorW5C9_rF9M@{quR>Lo_BxXI}emSe5Ce|Hwsb9tz+=E5&lK$&gIGM);v;$cVx~-2! z-R?Nl)>SbZp(fN3BXJmNE2pEzS%#X>7K<Ok1j^@8<9P2o!!d?pj8CQnnYySK4neJS z9O~>RqgFH*b&L07dAx=-F#M4F;cAZcC{IQm!3oq3eS^9i0f*T=EP}es+fbL;|Co&a z7~I6d7=Oe~s4d1(eig^yc6<qAj=JB0aj30LL><{&)QcBmH10<2#2ND%>g)O)wR4YM z+4sVaxs~Ncb*zJG&<S(l2-KF(K@GSTb6^T;=RQX5+#jfkJVi|`;JEvJ$cY*+4l`l{ z)Xp}>)c^l)dooc3dZGrHfI5OHs29w(atfBBe8~J2(^JlVf<IWON8`j0{4gy*d9NyX z9$D;b)PyF}E{jj$FROSPPmqF1g{a)1(RntI>L|lg%G&m)ZZi4TNXv-7MrurK9qBf) zwUi5x*TczrIf>1pyo&r+<kKEsnKwu;5PXdWRY_eaZzjc%GE%N(9e$^*=e{HLpGB$H z6OBi$T~_jIEdMuk8At`xi02mZB{(Ow#P|OjnNMtx!ML0TbFA}Oi&w_=#FB8I)y*W| zi`10<mq^9Qm!^&%LvIGAqdXR;;1ptd=2BivIqmr*U2T4N2tNPJrSK)O-ZbPcc{#{m zwFV1_Z6beyl#6`jq;&Dw{r$9<LFotFgd<3LN?>2h|Kvj6RP$GCMf>BVC#1CJAM)Gj zvmN^o9EV?!+K|Rl4z=-qCLioN@&BPC^9~L4B%AZBVb9d4`^P`9koZ!pPikasW?3J7 zZ`a6YwQ@hoX-^Si6)c_>tCw2l4i#gd$wWUVjU|7^TC}6V63QQ-z9l_-NChZoB0i7& zi&&WSm{<}i?fLJyykZ|we$it3GuV&(F_n3L>Q75`)U%o3yQCOZ^3)^^BF*L{SMVnB z)1-GOr#&sKZF^!nNO>rCNUKEsM6M;)m{j?>cz5#tX*z78;&svo1adz&@M3HHD|LfO zddguZ+GHl_r>q%9keU<EPq_v8N#uv&LOe;TPhL+C^1WTw`<NI%nZCE1Kv$C5%%*{! z)f85dPe(dQd5|i3YLRl2Dp|QDR<}N?|B?KQSeE!iEJyl<^eIWtOwvGWUochnf3ywO z&U~3hx7CnxZTy{vH>?vyFOYa$%Gs@bPkf8CfmlgWH`<KBL&PRxbsVRTJXa|Xby;sQ z`F=i?`Xj7opTnC^tO_ZN25%ADg;Pm=h+VffM<}O0SdsV?D{nFL5F2AnR*=79@s8vt zk*ZmppFl->3Z)1ZB<XobtS#wB@*k3Jl8RD3fX_c$D12yrYGWhHRY`x+R!>Geg<Dfg z?jLR78{&CM{6+G;yJV)?Am3BD#qxE@uUBJIR#GQYVN%+&j?6jw>e)^Tpx^4$lKV$` zt5azRd7re9{8p?*+acs1==)ow7CbdbnXJ=j%tE80#Fmp@w)l8r`$>B#ziF}Vl=U1o z4`4ycM{&5d>5QEz|3LgK>06SX@|x8=Qcem-NmWStSCK?wdq{KW^bTb`LF6wIdkLR^ z5-7YvT+eJ$Ve*%){0#FEi^rX$cI4kDg_Cbh^6T)Ay$qNg^^_;y5kp9)$bU@yZ>w8R z`3&iOQrdHmOg+lAX>*p;)Y_gV-pTT6Tb%rz=gJB6`7Ry%-&nQw<f6d>YxIQJXQT`? z`~zRd3)J->e+ZY7niI=QSx+4KPl-JxpPu~l&u+^53GAl+6|3k?{w1A%Uov`*kgkz( zQ+}ub&%fkP*kC&-r;sL)jt~nbT_*n>DW0^I@_Af_O-Xuoq5i#W4QU&xB}va$4&8tE z+%g~V!l86}OnDz3w8?#Ls@ZblHA&;GPcr$mrz`CaQW#Dw1Nn8NQ>2lkS4nz`JG_dN z{ST~jHVSLV=df}se8KY8-g}cwF&iYs>huG)lT?TFZ(2iKLc3UNGY-oTD~<Ju|3$td z4oJ`XTT_UoPzeJ_VKf>~ITEu|4k52+2B{kPKS;lk@{_ug`jbYGCemI{1o=fIJwM?` z#G6RUqsW)Db|0s<VE?O7Ie}mn0(CLYI^U%{oHT;emiT<!h-a)mFZpTYcM;dqoV18o zC1Qc(4_mvX#Qvk4k<^;}b>inyPrSBI&o+XkT{iWfEBR-B>RMQ=D)ni1<z3=MNF9hx zC#5|j$($qok60+Fo>jb!4QT)T(~J5$1pHU1$Y+E6@A($wUnf<gz7U4vCE8^opBZ1K zJdpe|QWxvLjo6=*^~@yJlk~pTm3Lup0_9QU&%3zq4Ip!zjzz4&0Ng?B5QdUUQui6< zS>*d*6Utwb*HegcAW6?ghnJK3>6VYc8q_CZSyCD5o_`)`Prs$XG%6}u%_rD|G@5jh zcv<R)kw1WMk=m2mQZ7dNf|T~WVS_icz)H$pNwJi>(WfR!Pk?z}>P#<^zNgS1E0B7T z%20lj&c{f}l%pu`#J;4}<bNimJx|FWq}^OnGlGq*ZY1R%Bt6p{UJ7;LmTy4bf5{q0 z;zm+8I)sp-701n_8l?9~Nra|j3F_WPJ<G@^lD|Rz7xl3x3|CTaW9=0Fn$(%pkk~NS z*7sJZh9}AzW_zx~3&dB@a6ah`(jd}cVnL{<B@UtetE2^#^Whj$7sXOr@Rg9)bHU+N zpj?vtDEh8VUy4tLz$5}g3GA`P?~z|+`FrHwqrPEEks1Rtr7W-eXP%T*-A2TwL`{t- LkWznLg_!>XO01q< delta 15590 zcmY+~2YgT0|Htu5BoZ>nkRXvrf*7$!ViT%n6s_8$jV(r`R@*D~Dkb)wwMS86meSIp zC`yYeRYhp4KU%cf|LdJ|@_6{)$K!Z@KIfi$&bjBD`=!6fZ^3wvzs7sGKLqBR?{J;T z>p0;!uZZKE^l%*Cc-1;iTwTWrz_RFtiRg<B(H~o*A9h8x8-Niw28-b$TfZB_$d6+< zUd6(W<2p|Xf~Y81FQ>wZL?7}hSP1K&9&CmAF$Fb&!RU!&Q0>Q|CNLc}u_YLan~;H= zW48Xh^(Oi-zVnbk5B`mfuwZ@1DT=MFeX%lm25M1uAx)hhZGDLb=6*6(qJA9e{!Y|H ze#HX#9KF%Ip|LOqx>OV+D2VZ>H%>$?X$#a+y>3lMz3E)kgqI;Z<!nXO??erF00Z%J z)SF*Lz0j{%0=;=lwJ(FNf+_@B$~vf-Ho_=ug?eDHt)GeNaG|Z=gnI52YQkTmUgWCv z4)!5`gxaz$jZM3LsENMSnDy6<NmS^;xu}`0K<#-JM&miu{YR+wPLi33A8L;yQD>+Y zYKdE-4r>SWz%Hm4?P2YYapY-9tba9vWmLrASuBr_Py<CZF~(bKqn5TAs>9Cs626JG zaSK+&+xC7)Q!}AR45mH~wZ-*NE7ry(&}Y&WtK&%25^qJl$r044KZ#m_GuA7p3H)O3 zKSZ60r>Ks7n3eKS)Qglstx$FA%c%a{t_0eP6zd37#}lmYp|)ZnY9c!^1hY^R|JvTa zi^a+FHe)-m1nPc$)E0F@ep;MisOP65{kqNy6FB=&51vJB#qXFOgI_iegrk<QDrzN~ zp*n1D>j$7#&b9aFpeD2&wG#VLEAY9^uVIM3{|5v*<sQw=K+&i@PC%_pOPhB_buieP zfqJv~r~$U2-s~{y5T8f2e}G!SJS|MSVyL_v7G-=Vi9m1CO%*u8HkgH4felyycc9+z z0IK7!P>1P9)bkHfZ|vRDoPk)(Suxar%~0)nqF!tyy50nn3G~LZF#^}14<1EL;0x4% z-(WtxjT$)H`VW>T_iJTLM6E;$df`~q#K)snY99LHx>l^eX7ZuEaTL|@S=3B#px)pK zszbllW~*Y*m%K4*;>kAejT&f(t<OL`HxIQkn^FCJhMLHg)~vrC$fiOAKS$;MZOq<A zp&m%Ucx;Uta4f3bEYulTWzE8Z<Y!T*|EjJ3%Nmeu>dT;3xUoy1C2ocO*a7444b(5+ zOpL`HSP?JS+@q~&ABUPiE!3MPqqZmowdVsc04JcfIureHnbqA)pf}1wE$Nr`#%0um zZrl5hQD?yW74w%;IBI1Qu@JUF-S1)ZG}MG9q9(A!-rtNm<VTQkUFR}^W|WO;_!Kp; zPdjt!!%!W?AP+c+sEM{hEpaFG#1S?hgIdw~sD76tzfH~-tcic1&Q{s>`n_TQ8xmBY zq9<za-b4KWEk>Q{!&nOMVj>1~FsHgXYKhyT2I`N6aU^OBrlI<qg_`gZ)QfFEeHDAr zN8kTpTX7P#M_;1{UPAr-e+47)32KGHUN!CFtW{7yV6{=7Zzt558Hpjd41;l(tv`vH z(0z0@^QQzd|7)gU2x^JSp*n74?TLZpE@}_wViDYmIs?aT{RPya{1vq#ULDQE!cYU( z#QK=jk@YV_Fy1!UjLJ`;X8b#92|ZsoOI8@QB4HScHBb|8jXDEeP)pnkHNl~%6`gMD zGf@**gIdWQue1I^1RqnO8J$P%U0^3uABvhtdDN0N#1KrucpQrwU=!vnF={1Fq9%43 zHL=^MJ-?6Y|9`0O|A}jZyq(RS`=Vx82*WS})lp3>f=z7R1@(*8AN4yi2{rIWEP(s5 zD4sy|^8;$&zfk@EgIWRCr;AzQqNp3CtW{ARHbBj^Eovg2QE%K2bry!9I+}=D`gy2+ z)}r=)r@j9P<|Y3IwNe+6=UwL>fjWGS+6u3(X2!)(dtVK;=M7K;c15)tVx3^?7oy&D zGwN&{$C7vjwQ|o<6AbQVCJ=)~^!+Cg=)q*vXVwq(=2KC7y%^Qu4vfH)w*F^S$A6>l z2X;3TD~DR@`l!86LiOL=+8gVWkHx%<?;Ie|3LHia{2A&O>=LSj>!>~b8MPw6TmP~3 z-aSlx5NazTQCm<6wFPzU{Whq6I-}a9psR-Iw!wJRDV>FtaXyCQXQ)H=6RN#ePqUOk zs4a;`by&mN6ibu8iW=~3)N@l%FEG!#sweBO8E&OQpVJZ4(%r|Z_z<-svAyg7s6*Hk z)nR+o;p~YTXaH&=>Gu94>s)((C2Gqyqxw14%Qbs<h6>I27t}=jdz-zkizUcAV_6)H zQMeXM;t5now^5(lBh-Zb-!O+P5{r>n$5NP#H8343;ue=cZ}2^8fLo|j>i?!Wj1gFl zydG+T{V*11V<hfD4R`@{R(`hi*{CyAFvZjdp_aZVdSNNlb8ZD&Q4c+-Xo7mM1?n?v zhkCFNYGT7tZ<K*r%4MkUc{gh5Phm7(Ms3mGsKf2u$GkvctW2JO{M5Tne}W2B%t1YP z1ogSxKrQJrtcAgS&0%bXTJrv=vojL4Mbl6#vJo}lDb&iIL$$kt+Oh{W59p^CWB*GM zXrOr1jGJK$c0ir}QRt2HQA@VM<~yue=t2E4%#WX<`Z<kSx$jZG54TY(`Y&oif&KY? zVSFc+Ko8W$I@k=$;{?oyJJ1XFptj(k&A&iR{2Z#oY}60dbJSJ@4KV+48-rT0j#wO1 zQ0+3%Ek&@9pe7!+Hy)uH78uCC=dd(t=95taE<t_wd$1s$M4f^27=d?e?ls8d#ZePV zL`}50waXyZzZw;TsnDCQM$PmC)P(k+CX$WX<NSloS5g|)VO!L|Jy9K}V=PWV9n!5> zA5Wq;dZn870a%GVHkI|)l6Ih?0KQ>OML+V<s6Cy6O>j19pbMx&dK)#t|4<VyKE!Ng zJJbYwpq6?F>g-HHe_Vj-cfCuXfwrM99>f4Vg__W1)ZYJuI+XWNZ|FbNe6H0{&nKc* zq8aMBuBa{Qi-mBE&1YaF`3lt5xkm{2(R02)&8)&O^Ig}$0P=RI`rfDs4nr;NyY~KK ztVF&E^?7}Vn&{uC_L0NQ{R*g+>WVr`qmYTa&P)O|T!Mvh18T4Kn+DD&*0ZRk`~mf* zk5C=?q?rjuU?6z~n>WDH<ZV!gG99&2nW#g&8jI`uKS<z1#U<3hH&6}lVH`d|y-~># z<}anDm`FYfE8s5F*|?56q_OE{;3U-V#{lGubf%yt@(?wFfVcQzV|=G9K|J<CeU7uS zJbr}yed64}=GbJU+4EVb3C}}Kcm)RI2Gm3kq0YdMSO~LG?fyZnu;(b#Pbj*YVFZCX zsEt~}WDLRHHXnt0a0aTw<*23IjylB`F&|z-ZRJf=``=J!CC_LxU^r@G(WnW<k7oUY z2x?NHj#^mTp$=7Nn@>l**<92i+lwA}8T;hnKeJJXHEFCFs6A>Tolq~-2laxZF$`y6 zQQR<=_16rK*&A1}9Qk7`i&1Zzm1u!Q$a|tX7>Szr`_^@+vyz2+ksGKLyo;XrAL`9L z-!Usw2z5W&CD0ospgQb~`rJ}&gLhC5W}=pK1t#N8ERN2*W<ueZL|OxN2uEW}T!aaD z3mahsFI)$EU>kIo66i49Lk;xE=KtB;XPlW>Flx`@P)k|`wIWSWFVMyM4yvD(s3qQv z`k^_2dV#YTi8qlIbDg{yW(&%rmi#5Gf<sXgTZ8IwD{9a8+4_U1U&2qZB;G@{^Br#n z3`MO_4C=WCsQXE%v(hFf=PkPv^rE6a>d<_Nn(^hF1}vTRH`Iilp*qet!Th}*g<A3i z)CzS(4b&60l><?4JO;JrOVA5<Vm`)q_SuR<s0kdmp2b?^*Dw)7CYs-cWUNj;7WG55 z6V>q%>jl&n-9t_IIqEFrpTvK_VtcHH^DyV%|ECDl@FHqYucP+rPgF<Vlg%$#anui0 z0_uJr^ukf7=UvnbOhc{U0`$T)=!u(96W)P({_te>UlnJl(2`w4?adwA@Cka6drdJ9 z_@Ul35>;OV^;}&njm=O4r=qrG4C;jz+k7WRke|U)_+Sd_uZBTW&A;obqCUg+s6Fe8 zx<3V@@dMNfd~Wamh55-tr<pg4M6E!)wKnSar5S1>y)oy92=$_~T!JWq752u*sD>9% zOP`H;<9{(6gQwfi57j{v)K78;Tc3g&a1?5wOw<q7MpVDYP%Cg5^<wUywn4}YGeAYu zz%5Y&cgIj1Ve^@&j@F<iv>(0kThyChM?HTZwUYl?gJznEl|c>M7-{c1y$IA{D(bVE zWL=H=d=8@y&sR482KDAQP-oyF7Q`p0H_rE-X<rm|$Rbe_se+ZTDaK<u1~b01mOyWi zh3fbi>dnugFaCm>$RpI7dCW2$7eRF#hkAj!*7m3+9)SKh71iHD)K+anP4tNB_5GhG z&`j@Q8T=cyXEC$Q19edoYmI8x%{l}%p$t^}c~}v*p$58Y?>|5d=)7;XEClspRnb)k z4G8?P9cn3ip#~a^KIo!O`7~6!IjFB;6~^Fk)EobTmC$pJd661eh`bwWf~ly(IRy*g znmMe$X10xr!uSblDKDT7T{h}LkGW<E!%-bpMJ;`jwIizCK<h-*=erzrNDrg>`yRCw ze_$YbXR`i!uz02!xH{^=mRJ@CqW%_}k9uGS=1dfW$S<HezJq$snP=KZp;oFkY68ts zhq#Z;2cgc~G?zecG#BgRTC9#YQA-;--*i|9RsSlg{dA1NrC1ja+51niGI_-XW-EK3 zCO8oNZ~_M79Mpu|4+ym9TQLB$P;Yh$bx5wFW_lNO2L7-<Lw&A!7n(OOfjSfQQ3Liu zot@FB=O&<jqGw<n?!p9p|5pffc!C$1rK*Z0$&*n_I1Dwxai}+#iJIUN48iRfiziSk zb{C7F-(q7dhLYDotz3K53cQ(9&;E}l(4Njg?crL~z{jyEeuFv-VN1+J8)H@So~VH` zQ4`vXI@L!|pYi8d6d$5~s0uDM_XANc9D&h{@6;jC1iGWPU?^&a<4}igK5BrCSO*Vb zK74}3@VPZ?nc0GBsENIXRd6tB3s<66Y$IwVj$qFJ{(pu*dv(qF3+jP~sEIv8P0V+> z`KMGk>P@3i1C_UVV=P79+SaFG6!~=2S=xenu`E=7r<b$;;RM&H(9E8p-mK6HvveV- znN~s#*bsFVTB5e7qrKk;)!|6%6jb|#HeZ7}JG)RXbQIO^H!E0w4g8aB@Dw$H{434r z_C-xF4)q32QE%K1Bd|Ye;3*h|3sD1P+58LC8OTOG=ef%K?H7*49k$aY(CO`nYWOy4 zi58%~f-R^APoo~Vh5F2#)uw$6hLP4oP4qR?=aq(fv1O<;vI|S#Y1BCPG3WdDT4TOX zUsOlIsE*>X1Gd1txDs_H)}SV|#pa)&mikN75`T*t_!g?&Gt^cUUTan=%32v|?>Y?$ zG;kYxqc7@#k*GbIjOutHYH8P_wqhS9;0Y{;&rxq4x6T~qS5RL~7u0h@tQn~OGqEz` zJ8KAZ`p=`@<eF`82Q|=NsLwFpdh<IFiuuSJVG_QCTAAsn31^}vyw17{)$dVz|4Unc z75y3CxksRZo}x~-*9YboD-bo)ayC!IO60F#70kdG+>NF264t<fQ9oGKH<<r{qC4u4 z&On`krKqhrfUbTT&k@MGs3pv^(fltO<uH+a2x=mmP!q^Pb$AEW;S<!rp_|NqptQzD z<fE}99z-4LYv_r8U^G72#QJM#if=ZDurzAVDxwcIMy*UM^u|u84tiq&?1wttX{h$I zP%F3yHNkbLiSI$3f#azDE~37so10nx`~?3};e}pX%x}A&wINofKFzug^~M*ik5Gp< zXsh}Ed{w|$@&Tv`%toD^m8jo~UDmUx3IFO6=*^y>-Xza9^Aj45T8YM}H+T)>F$MJ| znHY)du@rua>M$GieLqG$7qH#*UjjYI%b+ioNA>R}5ELM2g*v?*Q8Vv{n)xVH!x^Xt zSE5e+R*b{nQ3Ds<VYVg){mILs`b|K!OG5QC2=)6g&geP|2x?KW6$9`VY9$_719qAv zEr)v3WYiX<qYlwL?1TGI9hdmf+^>#0)a_CI4n&>$u~-e~Vo`nnpAo3zA5k56?J^C+ zQJ+^eY=9k6Z@Lh*605N#Zb$9yGt_Ah+igD2+Nk~pp#~n0IxCA&hi?}~GQRUQfquz; zML#UF$LwJZ>`I=Dg>frt0!J_yPoXAq&DP(;w&agc12_H14Acd+Vk5C8E<l~R)97li zz9rBS{fx>VqXx{k*L)pCP!lU*t$=~#wXq<!Mtwb<unhLJPDM>{C#v7Ws5k!#_1vAk ztbYi>BPxQhz&>LM)L%kXQ5_^>E9{Dz`6hdR7wY*lsL%LkRL2ichwdqAqS0AQ7HeQR z9E#f7rCF@MX7(`^I@Moe6b9}$KdIHQ5P4_R3Jk<x9F02NvrsFy*t!Wdq5W6{&!ATF zHmaW|s0sNTF!kXsK~*X$qB`nj9fncl8CVwApa%R3^`@6lD{~$7M)y&NIP{=7-Sx08 z`7o@E+p#fT$Kn`$$gGfCpFoGBFHXeCsMB2VusO|Tu?cxg)RMiAn$RAM#~*M6`X1pw zPA~&K@e*ofuc5Z=K5F2{7>0#E&RGf9DQ5zw9_n<rMJ-)tn-4?1*;v$rtL^;*Sd{!6 zYRR)vOPlAY*`i?7M9ZR9u03iZ-BA<ki=q1d-y+a}vrvDpuSYHIX4HiCVJIF$b?_Z( z3vQwYxM%a=W9DzaaO=yMm-{0y59(4s(Nvzz$@qiu9X|V<>zQqJ4K<;0G%QH`#MZCI z;}l;?Ece!{cdm7mh6=bU*tY$+=Mra77Ezx;X-VB$%CFR|CNHTr=&*%O5h|xsv7Goa z@r&2{B+aNB&;2@-uH+jjQ4}xo2Dbh0<huSa<op@OeO<-ysBKr6c$JA==Lt7GD3NN# z^?(}-a7IqX^5SXR$sk<J{Ta5SuWWrHuA^=~?z8u%5x+rc#q;MW<%r|C_Yv_F^dNsL zPY?dflwdrSx@K}?0eS9qn!GjT#cL+XCF<U!-BIEY;_q#P_o>@J{0XHvaYE*eia~A` zEvAy)!i_kbqN_aivGLEQ%9&ujgRjv36Ux7o-0L6WZ9KCL-=uOReoJXjd5b*2_II0D zKZRVbbBsT?(?Hh->nz)_m#NPA^Kbslq<$ebp|r4VrrBroz5PI3$maTcH}@({T@72G zTbGoRbH>Wjk&fC0%1GibY>QWEu#o&y)VHK-7bS{3KlSesH^)+xzp0x?$-VMW_u}<A z_g=R5U*Sd{;$zA+us-BE6kVIB+)F8`n_TrM11K}-@H@Ou{VB?Ja&J3GvTgf1`3_1r z`RlaJy_yoNp{@m`R&FJ7z8v=y4{YSdM9K*&!fmH&H(!5n-R0guims~Ig*F8#`rD?B zq_m;F1bJKHw~2@1Ts%Q(O026V@f#*|zMw8l@Bbkc-6(1^od&vAlB^)kOF2$H&~{Lt zxELkD=C5E~`wZ32&%`fdRqDrJ70NG^#uQysDgABx=p0%9blcf$)>IljpbQ{yfDdVS z&pt%z_)y=NJjAx|jms$OsVh(EPMdT*Ox+l)ha>eM*A?=?CUoW#_jS2h*jDVd&Ze$5 zC4dG?srwKoP*SM7Y1@2Et}74LqJF&1H(Mj9OE*oNrNrOc`p(4fP-@$IZZ&RnB#Eao zhN9~)>N-$v5r0a_rj#M)9|-^N+C=u5eWnpMCr_k2rmZed{0ukeWaf|daGm-HN_p-7 z?*tQVCpXkMw?pE!lpiSi*GFec3?=tkL+~}v>e@<KPP`%~Gk<v4d&&k8kE6^b-hvGo zV-WFUeSev1!Bv;yWgi-WzBDRL-C|0rtshNY7G)3l99!3uT-Oon0W3*=6o=Y2U9c<p zPt<=&xlYklU4IY0M+qVMn36!zKSkc5ZZ~Bn4{amYRe<<Bbsh1=t18J@>UB-0#1emJ zbBDGO)Kw<mNqLp{Af+&IJBr(gKlacl7<JVk?u5RSlf++8|J2@FNB$+{5GD6|NYI45 zA#J{*w6bkaQs3FeYFn20w-@s2JaZ!t>)%3m?Ny8hvu&g2)SaVv((r#c3D0t`C-Gri zNJ*wHoLpBW;&asfOPr7R#cLP&J}P!`f2_Un2Jvg!{}cjUM<~}Q#mWCv1=n-p<F>Qy z<a;QiDIZhkN4ZS=Bc&2$3;EZ$7+X_xeTd$;lCp);mZIyDLFeCG_pN_2U@8wiBj1My z?c^?6)ocm%4Jf1RGaHC=uWqzEKr)QFe8g)hCn>`z?^1Nd8JwEr?x*(QK$2C&MQq*< zU$(Jr@66G|wv&(Sy$sw*X-xSiw;?W|U1{596jr3JA~vD^FXB$vKQHg!j-(VxP4uD^ zqR}YwFbpF1Bi1#AQiu2vC7TjS=|SmB8AcgPdtE`qnG{{O@pI~1;wkbG#MNxO&vRO^ z{)yZiLuCOflCYwE_&4&Qlwp)tsh@=(;FtD(3F1k_JE_;zhBA-3TGZtyK4RM~qRt`C z2knV}B0i%!t}6Qd*R_?(3MS0?a~Xd|aWC1{)#m;<8}FsQ4CQs|CR1{+;RL5CSE=)- zB-tCQ@Fm)R_Cnj<#J^JSzRisi_Qn<aKrRyr>T*9C1MvdwJc$e75c2-S|53Wy=eJV# zh+NlH>UvQQ*?ZMYwKJMLjrfeIcb$F&pYUKQ+n^urpzaX*Q7Uln8}ezyZ(=L*Z;5rq zkb6^ftv5KK+@EaYK&;38cd)Vw_g=i7>iheV29vo_!`}G{TT(_)PEcQk`$LEi;8IFQ zN(b^Xl#7(yYmV*wC0ntaygMb9ygSd-r|8OW{h!qL{|)6PNnfl^=}oCbK9`4&Q9d9K zBj153l$FH4P;#&Th!4{4JxXgTo7;P7<h>}mCK;TMxL3%=O^MytY~xV;fYO5p{3%6= z^Wr8-eae2yJRY2a<+!&RbuA)(m-sI69pc=}pI{kz2is2dS1Da6&8Qn<+PcmX-Qg-~ z8wS1bKr`x>&~O&zeaZmJK<a!@S6duRdwoT-$s_SCN;k^8IW731A=Y)q;M629N1Vp9 zYx0)oH=ByLsTfSfZrgZ2@d_LNL7c_?mv(#B?dR!}GH6I@-?XR>Lv{~r^tia+@RYRl zK_k+l`V2`=eRFr$ZX;p~G)w7seDClPQDf3~pUDV{3~D;OZ%Xgf{%L6`QGHYT&@3f& K_mx$3qW%va6uqVZ diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po index 95c8b95fb6..493d4a8b06 100644 --- a/InvenTree/locale/de/LC_MESSAGES/django.po +++ b/InvenTree/locale/de/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-09-28 12:03+0000\n" +"POT-Creation-Date: 2020-10-04 14:02+0000\n" "PO-Revision-Date: 2020-05-03 11:32+0200\n" "Last-Translator: Christian Schlüter <chschlue@gmail.com>\n" "Language-Team: C <kde-i18n-doc@kde.org>\n" @@ -92,7 +92,7 @@ msgstr "Datei zum Anhängen auswählen" msgid "File comment" msgstr "Datei-Kommentar" -#: InvenTree/models.py:68 templates/js/stock.html:690 +#: InvenTree/models.py:68 templates/js/stock.html:699 msgid "User" msgstr "Benutzer" @@ -107,19 +107,19 @@ msgstr "Name" msgid "Description (optional)" msgstr "Firmenbeschreibung" -#: InvenTree/settings.py:341 +#: InvenTree/settings.py:342 msgid "English" msgstr "Englisch" -#: InvenTree/settings.py:342 +#: InvenTree/settings.py:343 msgid "German" msgstr "Deutsch" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:344 msgid "French" msgstr "Französisch" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:345 msgid "Polish" msgstr "Polnisch" @@ -151,7 +151,8 @@ msgstr "Verloren" msgid "Returned" msgstr "Zurückgegeben" -#: InvenTree/status_codes.py:136 order/templates/order/sales_order_base.html:98 +#: InvenTree/status_codes.py:136 +#: order/templates/order/sales_order_base.html:103 msgid "Shipped" msgstr "Versendet" @@ -206,7 +207,7 @@ msgstr "Überschuss darf 100% nicht überschreiten" msgid "Overage must be an integer value or a percentage" msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" -#: InvenTree/views.py:639 +#: InvenTree/views.py:661 msgid "Database Statistics" msgstr "Datenbankstatistiken" @@ -266,7 +267,7 @@ msgstr "Standort-Details" msgid "Serial numbers" msgstr "Seriennummer" -#: build/forms.py:64 stock/forms.py:107 +#: build/forms.py:64 stock/forms.py:111 msgid "Enter unique serial numbers (or leave blank)" msgstr "Eindeutige Seriennummern eingeben (oder leer lassen)" @@ -280,7 +281,7 @@ msgstr "Bau-Fertigstellung bestätigen" msgid "Build quantity must be integer value for trackable parts" msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" -#: build/models.py:73 build/templates/build/build_base.html:65 +#: build/models.py:73 build/templates/build/build_base.html:70 msgid "Build Title" msgstr "Bau-Titel" @@ -288,7 +289,7 @@ msgstr "Bau-Titel" msgid "Brief description of the build" msgstr "Kurze Beschreibung des Baus" -#: build/models.py:84 build/templates/build/build_base.html:86 +#: build/models.py:84 build/templates/build/build_base.html:91 msgid "Parent Build" msgstr "Eltern-Bau" @@ -298,18 +299,17 @@ msgstr "Eltern-Bau, dem dieser Bau zugewiesen ist" #: build/models.py:90 build/templates/build/allocate.html:329 #: build/templates/build/auto_allocate.html:19 -#: build/templates/build/build_base.html:70 +#: build/templates/build/build_base.html:75 #: build/templates/build/detail.html:22 order/models.py:501 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:147 -#: order/templates/order/receive_parts.html:19 part/models.py:241 +#: order/templates/order/receive_parts.html:19 part/models.py:293 #: part/templates/part/part_app_base.html:7 -#: part/templates/part/set_category.html:13 -#: stock/templates/stock/item_installed.html:60 -#: templates/InvenTree/search.html:123 templates/js/barcode.html:336 -#: templates/js/bom.html:124 templates/js/build.html:47 -#: templates/js/company.html:137 templates/js/part.html:223 -#: templates/js/stock.html:421 +#: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 +#: templates/js/barcode.html:336 templates/js/bom.html:124 +#: templates/js/build.html:47 templates/js/company.html:137 +#: templates/js/part.html:184 templates/js/part.html:289 +#: templates/js/stock.html:421 templates/js/stock.html:977 msgid "Part" msgstr "Teil" @@ -345,7 +345,7 @@ msgstr "Bau-Anzahl" msgid "Number of parts to build" msgstr "Anzahl der zu bauenden Teile" -#: build/models.py:128 part/templates/part/part_base.html:142 +#: build/models.py:128 part/templates/part/part_base.html:145 msgid "Build Status" msgstr "Bau-Status" @@ -353,7 +353,7 @@ msgstr "Bau-Status" msgid "Build status code" msgstr "Bau-Statuscode" -#: build/models.py:136 stock/models.py:371 +#: build/models.py:136 stock/models.py:387 msgid "Batch Code" msgstr "Losnummer" @@ -364,12 +364,12 @@ msgstr "Chargennummer für diese Bau-Ausgabe" #: build/models.py:155 build/templates/build/detail.html:55 #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 -#: part/templates/part/detail.html:80 part/templates/part/part_base.html:89 -#: stock/models.py:365 stock/templates/stock/item_base.html:232 +#: part/templates/part/detail.html:80 part/templates/part/part_base.html:92 +#: stock/models.py:381 stock/templates/stock/item_base.html:244 msgid "External Link" msgstr "Externer Link" -#: build/models.py:156 stock/models.py:367 +#: build/models.py:156 stock/models.py:383 msgid "Link to external URL" msgstr "Link zu einer externen URL" @@ -377,10 +377,10 @@ msgstr "Link zu einer externen URL" #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:15 #: order/templates/order/purchase_order_detail.html:202 #: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:67 -#: stock/forms.py:281 stock/forms.py:309 stock/models.py:433 -#: stock/models.py:1353 stock/templates/stock/tabs.html:26 -#: templates/js/barcode.html:391 templates/js/bom.html:219 -#: templates/js/stock.html:116 templates/js/stock.html:534 +#: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 +#: stock/models.py:1404 stock/templates/stock/tabs.html:26 +#: templates/js/barcode.html:391 templates/js/bom.html:223 +#: templates/js/stock.html:116 templates/js/stock.html:543 msgid "Notes" msgstr "Notizen" @@ -425,7 +425,7 @@ msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" #: build/templates/build/allocate.html:17 #: company/templates/company/detail_part.html:18 order/views.py:779 -#: part/templates/part/category.html:107 +#: part/templates/part/category.html:112 msgid "Order Parts" msgstr "Teile bestellen" @@ -441,24 +441,24 @@ msgstr "Automatisches Zuweisen" msgid "Unallocate" msgstr "Zuweisung aufheben" -#: build/templates/build/allocate.html:87 templates/stock_table.html:8 +#: build/templates/build/allocate.html:87 templates/stock_table.html:9 msgid "New Stock Item" msgstr "Neues Lagerobjekt" -#: build/templates/build/allocate.html:88 stock/views.py:1327 +#: build/templates/build/allocate.html:88 stock/views.py:1428 msgid "Create new Stock Item" msgstr "Neues Lagerobjekt hinzufügen" #: build/templates/build/allocate.html:170 #: order/templates/order/sales_order_detail.html:68 -#: order/templates/order/sales_order_detail.html:150 stock/models.py:359 -#: stock/templates/stock/item_base.html:148 +#: order/templates/order/sales_order_detail.html:150 stock/models.py:375 +#: stock/templates/stock/item_base.html:156 msgid "Serial Number" msgstr "Seriennummer" #: build/templates/build/allocate.html:172 #: build/templates/build/auto_allocate.html:20 -#: build/templates/build/build_base.html:75 +#: build/templates/build/build_base.html:80 #: build/templates/build/detail.html:27 #: company/templates/company/supplier_part_pricing.html:71 #: order/templates/order/order_wizard/select_parts.html:32 @@ -467,22 +467,22 @@ msgstr "Seriennummer" #: order/templates/order/sales_order_detail.html:152 #: part/templates/part/allocation.html:16 #: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:80 +#: part/templates/part/sale_prices.html:80 stock/forms.py:297 #: stock/templates/stock/item_base.html:26 #: stock/templates/stock/item_base.html:32 -#: stock/templates/stock/item_base.html:154 +#: stock/templates/stock/item_base.html:162 #: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.html:338 #: templates/js/bom.html:162 templates/js/build.html:58 -#: templates/js/stock.html:681 +#: templates/js/stock.html:690 templates/js/stock.html:905 msgid "Quantity" msgstr "Anzahl" #: build/templates/build/allocate.html:186 -#: build/templates/build/auto_allocate.html:21 stock/forms.py:279 -#: stock/templates/stock/item_base.html:186 +#: build/templates/build/auto_allocate.html:21 stock/forms.py:336 +#: stock/templates/stock/item_base.html:198 #: stock/templates/stock/stock_adjust.html:17 -#: templates/InvenTree/search.html:173 templates/js/barcode.html:337 -#: templates/js/stock.html:512 +#: templates/InvenTree/search.html:183 templates/js/barcode.html:337 +#: templates/js/stock.html:518 msgid "Location" msgstr "Standort" @@ -496,7 +496,7 @@ msgstr "Lagerobjekt-Standort bearbeiten" msgid "Delete stock allocation" msgstr "Zuweisung löschen" -#: build/templates/build/allocate.html:238 templates/js/bom.html:330 +#: build/templates/build/allocate.html:238 templates/js/bom.html:334 msgid "No BOM items found" msgstr "Keine BOM-Einträge gefunden" @@ -505,12 +505,12 @@ msgstr "Keine BOM-Einträge gefunden" #: company/templates/company/supplier_part_detail.html:27 #: order/templates/order/purchase_order_detail.html:159 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 -#: stock/templates/stock/item_installed.html:83 -#: templates/InvenTree/search.html:137 templates/js/bom.html:147 +#: templates/InvenTree/search.html:147 templates/js/bom.html:147 #: templates/js/company.html:56 templates/js/order.html:159 #: templates/js/order.html:234 templates/js/part.html:120 -#: templates/js/part.html:279 templates/js/part.html:460 -#: templates/js/stock.html:444 templates/js/stock.html:662 +#: templates/js/part.html:203 templates/js/part.html:345 +#: templates/js/part.html:526 templates/js/stock.html:444 +#: templates/js/stock.html:671 msgid "Description" msgstr "Beschreibung" @@ -520,8 +520,8 @@ msgstr "Beschreibung" msgid "Reference" msgstr "Referenz" -#: build/templates/build/allocate.html:347 part/models.py:1348 -#: templates/js/part.html:464 templates/js/table_filters.html:121 +#: build/templates/build/allocate.html:347 part/models.py:1401 +#: templates/js/part.html:530 templates/js/table_filters.html:121 msgid "Required" msgstr "benötigt" @@ -573,7 +573,7 @@ msgstr "Lagerobjekt wurde zugewiesen" #: build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 -#: stock/templates/stock/item_base.html:211 templates/js/build.html:39 +#: stock/templates/stock/item_base.html:223 templates/js/build.html:39 #: templates/navbar.html:20 msgid "Build" msgstr "Bau" @@ -586,40 +586,51 @@ msgstr "Dieser Bau ist der Bestellung zugeordnet" msgid "This build is a child of Build" msgstr "Dieser Bau ist Kind von Bau" -#: build/templates/build/build_base.html:61 build/templates/build/detail.html:9 +#: build/templates/build/build_base.html:39 +#: company/templates/company/company_base.html:27 +#: order/templates/order/order_base.html:28 +#: order/templates/order/sales_order_base.html:38 +#: part/templates/part/category.html:13 part/templates/part/part_base.html:32 +#: stock/templates/stock/item_base.html:69 +#: stock/templates/stock/location.html:12 +#, fuzzy +#| msgid "Admin" +msgid "Admin view" +msgstr "Admin" + +#: build/templates/build/build_base.html:66 build/templates/build/detail.html:9 msgid "Build Details" msgstr "Bau-Status" -#: build/templates/build/build_base.html:80 +#: build/templates/build/build_base.html:85 #: build/templates/build/detail.html:42 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:264 -#: stock/templates/stock/item_installed.html:111 -#: templates/InvenTree/search.html:165 templates/js/barcode.html:42 -#: templates/js/build.html:63 templates/js/order.html:164 -#: templates/js/order.html:239 templates/js/stock.html:499 +#: stock/templates/stock/item_base.html:276 templates/InvenTree/search.html:175 +#: templates/js/barcode.html:42 templates/js/build.html:63 +#: templates/js/order.html:164 templates/js/order.html:239 +#: templates/js/stock.html:505 templates/js/stock.html:913 msgid "Status" msgstr "Status" -#: build/templates/build/build_base.html:93 order/models.py:499 +#: build/templates/build/build_base.html:98 order/models.py:499 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:174 templates/js/order.html:213 +#: stock/templates/stock/item_base.html:186 templates/js/order.html:213 msgid "Sales Order" msgstr "Bestellung" -#: build/templates/build/build_base.html:99 +#: build/templates/build/build_base.html:104 msgid "BOM Price" msgstr "Stücklistenpreis" -#: build/templates/build/build_base.html:104 +#: build/templates/build/build_base.html:109 msgid "BOM pricing is incomplete" msgstr "Stücklistenbepreisung ist unvollständig" -#: build/templates/build/build_base.html:107 +#: build/templates/build/build_base.html:112 msgid "No pricing information" msgstr "Keine Preisinformation" @@ -676,15 +687,15 @@ msgid "Stock can be taken from any available location." msgstr "Bestand kann jedem verfügbaren Lagerort entnommen werden." #: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:204 -#: stock/templates/stock/item_installed.html:119 templates/js/stock.html:507 -#: templates/js/table_filters.html:34 templates/js/table_filters.html:100 +#: stock/templates/stock/item_base.html:216 templates/js/stock.html:513 +#: templates/js/stock.html:920 templates/js/table_filters.html:34 +#: templates/js/table_filters.html:100 msgid "Batch" msgstr "Los" #: build/templates/build/detail.html:61 -#: order/templates/order/order_base.html:93 -#: order/templates/order/sales_order_base.html:92 templates/js/build.html:71 +#: order/templates/order/order_base.html:98 +#: order/templates/order/sales_order_base.html:97 templates/js/build.html:71 msgid "Created" msgstr "Erstellt" @@ -798,7 +809,7 @@ msgstr "Baufertigstellung bestätigen" msgid "Invalid location selected" msgstr "Ungültige Ortsauswahl" -#: build/views.py:296 stock/views.py:1520 +#: build/views.py:296 stock/views.py:1621 #, python-brace-format msgid "The following serial numbers already exist: ({sn})" msgstr "Die folgende Seriennummer existiert bereits: ({sn})" @@ -913,7 +924,7 @@ msgstr "Beschreibung des Teils" msgid "Description of the company" msgstr "Firmenbeschreibung" -#: company/models.py:91 company/templates/company/company_base.html:48 +#: company/models.py:91 company/templates/company/company_base.html:53 #: templates/js/company.html:61 msgid "Website" msgstr "Website" @@ -922,7 +933,7 @@ msgstr "Website" msgid "Company website URL" msgstr "Firmenwebsite" -#: company/models.py:94 company/templates/company/company_base.html:55 +#: company/models.py:94 company/templates/company/company_base.html:60 msgid "Address" msgstr "Adresse" @@ -940,7 +951,7 @@ msgstr "Kontakt-Tel." msgid "Contact phone number" msgstr "Kontakt-Tel." -#: company/models.py:101 company/templates/company/company_base.html:69 +#: company/models.py:101 company/templates/company/company_base.html:74 msgid "Email" msgstr "Email" @@ -948,7 +959,7 @@ msgstr "Email" msgid "Contact email address" msgstr "Kontakt-Email" -#: company/models.py:104 company/templates/company/company_base.html:76 +#: company/models.py:104 company/templates/company/company_base.html:81 msgid "Contact" msgstr "Kontakt" @@ -972,8 +983,8 @@ msgstr "Kaufen Sie Teile von dieser Firma?" msgid "Does this company manufacture parts?" msgstr "Produziert diese Firma Teile?" -#: company/models.py:279 stock/models.py:319 -#: stock/templates/stock/item_base.html:140 +#: company/models.py:279 stock/models.py:335 +#: stock/templates/stock/item_base.html:148 msgid "Base Part" msgstr "Basisteil" @@ -1025,12 +1036,12 @@ msgstr "Zugewiesen" msgid "Company" msgstr "Firma" -#: company/templates/company/company_base.html:42 +#: company/templates/company/company_base.html:47 #: company/templates/company/detail.html:8 msgid "Company Details" msgstr "Firmendetails" -#: company/templates/company/company_base.html:62 +#: company/templates/company/company_base.html:67 msgid "Phone" msgstr "Telefon" @@ -1044,16 +1055,16 @@ msgstr "Hersteller" #: company/templates/company/detail.html:21 #: company/templates/company/supplier_part_base.html:66 #: company/templates/company/supplier_part_detail.html:21 -#: order/templates/order/order_base.html:74 +#: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:239 templates/js/company.html:48 +#: stock/templates/stock/item_base.html:251 templates/js/company.html:48 #: templates/js/company.html:162 templates/js/order.html:146 msgid "Supplier" msgstr "Zulieferer" #: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:73 stock/models.py:354 -#: stock/models.py:355 stock/templates/stock/item_base.html:161 +#: order/templates/order/sales_order_base.html:78 stock/models.py:370 +#: stock/models.py:371 stock/templates/stock/item_base.html:169 #: templates/js/company.html:40 templates/js/order.html:221 msgid "Customer" msgstr "Kunde" @@ -1069,18 +1080,18 @@ msgstr "Neues Zuliefererteil anlegen" #: company/templates/company/detail_part.html:13 #: order/templates/order/purchase_order_detail.html:67 -#: part/templates/part/supplier.html:13 templates/js/stock.html:788 +#: part/templates/part/supplier.html:13 templates/js/stock.html:797 msgid "New Supplier Part" msgstr "Neues Zulieferer-Teil" #: company/templates/company/detail_part.html:15 -#: part/templates/part/category.html:104 part/templates/part/supplier.html:15 -#: stock/templates/stock/item_installed.html:16 templates/stock_table.html:10 +#: part/templates/part/category.html:109 part/templates/part/supplier.html:15 +#: templates/stock_table.html:11 msgid "Options" msgstr "Optionen" #: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:107 +#: part/templates/part/category.html:112 #, fuzzy #| msgid "Order part" msgid "Order parts" @@ -1097,7 +1108,7 @@ msgid "Delete Parts" msgstr "Teile löschen" #: company/templates/company/detail_part.html:43 -#: part/templates/part/category.html:102 templates/js/stock.html:782 +#: part/templates/part/category.html:107 templates/js/stock.html:791 msgid "New Part" msgstr "Neues Teil" @@ -1129,8 +1140,8 @@ msgstr "Zuliefererbestand" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:101 part/templates/part/category.html:108 -#: part/templates/part/stock.html:51 templates/stock_table.html:5 +#: part/templates/part/category.html:106 part/templates/part/category.html:113 +#: part/templates/part/stock.html:51 templates/stock_table.html:6 msgid "Export" msgstr "Exportieren" @@ -1187,8 +1198,8 @@ msgid "New Sales Order" msgstr "Neuer Auftrag" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:328 -#: stock/templates/stock/item_base.html:244 templates/js/company.html:178 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:344 +#: stock/templates/stock/item_base.html:256 templates/js/company.html:178 msgid "Supplier Part" msgstr "Zulieferer-Teil" @@ -1240,7 +1251,7 @@ msgid "Pricing Information" msgstr "Preisinformationen ansehen" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2108 +#: part/templates/part/sale_prices.html:13 part/views.py:2149 msgid "Add Price Break" msgstr "Preisstaffel hinzufügen" @@ -1252,7 +1263,7 @@ msgid "No price break information found" msgstr "Keine Firmeninformation gefunden" #: company/templates/company/supplier_part_pricing.html:76 -#: part/templates/part/sale_prices.html:85 templates/js/bom.html:203 +#: part/templates/part/sale_prices.html:85 templates/js/bom.html:207 msgid "Price" msgstr "Preis" @@ -1280,9 +1291,8 @@ msgstr "Bepreisung" #: company/templates/company/supplier_part_tabs.html:8 #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 -#: stock/templates/stock/item_installed.html:91 -#: stock/templates/stock/location.html:12 templates/InvenTree/search.html:145 -#: templates/js/part.html:124 templates/js/part.html:306 +#: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 +#: templates/js/part.html:124 templates/js/part.html:372 #: templates/js/stock.html:452 templates/navbar.html:19 msgid "Stock" msgstr "Lagerbestand" @@ -1292,9 +1302,10 @@ msgid "Orders" msgstr "Bestellungen" #: company/templates/company/tabs.html:9 -#: order/templates/order/receive_parts.html:14 part/models.py:242 -#: part/templates/part/cat_link.html:7 part/templates/part/category.html:83 -#: templates/navbar.html:18 templates/stats.html:8 templates/stats.html:17 +#: order/templates/order/receive_parts.html:14 part/models.py:294 +#: part/templates/part/cat_link.html:7 part/templates/part/category.html:88 +#: part/templates/part/category_tabs.html:6 templates/navbar.html:18 +#: templates/stats.html:8 templates/stats.html:17 msgid "Parts" msgstr "Teile" @@ -1363,7 +1374,7 @@ msgstr "Firma gelöscht" msgid "Edit Supplier Part" msgstr "Zuliefererteil bearbeiten" -#: company/views.py:269 templates/js/stock.html:789 +#: company/views.py:269 templates/js/stock.html:798 msgid "Create new Supplier Part" msgstr "Neues Zuliefererteil anlegen" @@ -1371,17 +1382,17 @@ msgstr "Neues Zuliefererteil anlegen" msgid "Delete Supplier Part" msgstr "Zuliefererteil entfernen" -#: company/views.py:404 part/views.py:2112 +#: company/views.py:404 part/views.py:2153 #, fuzzy #| msgid "Add Price Break" msgid "Added new price break" msgstr "Preisstaffel hinzufügen" -#: company/views.py:441 part/views.py:2157 +#: company/views.py:441 part/views.py:2198 msgid "Edit Price Break" msgstr "Preisstaffel bearbeiten" -#: company/views.py:456 part/views.py:2171 +#: company/views.py:456 part/views.py:2212 msgid "Delete Price Break" msgstr "Preisstaffel löschen" @@ -1422,11 +1433,11 @@ msgid "Mark order as complete" msgstr "Bestellung als vollständig markieren" #: order/forms.py:46 order/forms.py:57 -#: order/templates/order/sales_order_base.html:49 +#: order/templates/order/sales_order_base.html:54 msgid "Cancel order" msgstr "Bestellung stornieren" -#: order/forms.py:68 order/templates/order/sales_order_base.html:46 +#: order/forms.py:68 order/templates/order/sales_order_base.html:51 msgid "Ship order" msgstr "Bestellung versenden" @@ -1487,7 +1498,7 @@ msgid "Date order was completed" msgstr "Bestellung als vollständig markieren" #: order/models.py:185 order/models.py:259 part/views.py:1304 -#: stock/models.py:239 stock/models.py:754 +#: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "Anzahl muss größer Null sein" @@ -1525,7 +1536,7 @@ msgstr "Position - Notizen" #: order/models.py:466 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:23 -#: stock/templates/stock/item_base.html:218 templates/js/order.html:138 +#: stock/templates/stock/item_base.html:230 templates/js/order.html:138 msgid "Purchase Order" msgstr "Kaufvertrag" @@ -1567,32 +1578,32 @@ msgstr "Zuordnungsanzahl eingeben" msgid "Are you sure you want to delete this attachment?" msgstr "Sind Sie sicher, dass Sie diesen Anhang löschen wollen?" -#: order/templates/order/order_base.html:59 +#: order/templates/order/order_base.html:64 msgid "Purchase Order Details" msgstr "Bestelldetails" -#: order/templates/order/order_base.html:64 -#: order/templates/order/sales_order_base.html:63 +#: order/templates/order/order_base.html:69 +#: order/templates/order/sales_order_base.html:68 msgid "Order Reference" msgstr "Bestellreferenz" -#: order/templates/order/order_base.html:69 -#: order/templates/order/sales_order_base.html:68 +#: order/templates/order/order_base.html:74 +#: order/templates/order/sales_order_base.html:73 msgid "Order Status" msgstr "Bestellstatus" -#: order/templates/order/order_base.html:80 templates/js/order.html:153 +#: order/templates/order/order_base.html:85 templates/js/order.html:153 msgid "Supplier Reference" msgstr "Zuliefererreferenz" -#: order/templates/order/order_base.html:99 +#: order/templates/order/order_base.html:104 msgid "Issued" msgstr "Aufgegeben" -#: order/templates/order/order_base.html:106 +#: order/templates/order/order_base.html:111 #: order/templates/order/purchase_order_detail.html:182 #: order/templates/order/receive_parts.html:22 -#: order/templates/order/sales_order_base.html:105 +#: order/templates/order/sales_order_base.html:110 msgid "Received" msgstr "Empfangen" @@ -1672,14 +1683,14 @@ msgstr "Bestellpositionen" #: order/templates/order/purchase_order_detail.html:38 #: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:153 part/templates/part/category.html:194 -#: templates/js/stock.html:794 +#: part/templates/part/category.html:161 part/templates/part/category.html:202 +#: templates/js/stock.html:803 msgid "New Location" msgstr "Neuer Standort" #: order/templates/order/purchase_order_detail.html:39 #: order/templates/order/purchase_order_detail.html:119 -#: stock/templates/stock/location.html:16 +#: stock/templates/stock/location.html:21 msgid "Create new stock location" msgstr "Neuen Lagerort anlegen" @@ -1714,7 +1725,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:132 templates/js/part.html:322 +#: part/templates/part/part_base.html:135 templates/js/part.html:388 msgid "On Order" msgstr "bestellt" @@ -1732,15 +1743,15 @@ msgstr "" msgid "This SalesOrder has not been fully allocated" msgstr "Dieser Auftrag ist nicht vollständig zugeordnet" -#: order/templates/order/sales_order_base.html:42 +#: order/templates/order/sales_order_base.html:47 msgid "Packing List" msgstr "Packliste" -#: order/templates/order/sales_order_base.html:58 +#: order/templates/order/sales_order_base.html:63 msgid "Sales Order Details" msgstr "Auftragsdetails" -#: order/templates/order/sales_order_base.html:79 templates/js/order.html:228 +#: order/templates/order/sales_order_base.html:84 templates/js/order.html:228 msgid "Customer Reference" msgstr "Kundenreferenz" @@ -1952,12 +1963,12 @@ msgstr "Zuordnung bearbeiten" msgid "Remove allocation" msgstr "Zuordnung entfernen" -#: part/bom.py:138 part/templates/part/category.html:50 +#: part/bom.py:138 part/templates/part/category.html:55 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "Standard-Lagerort" -#: part/bom.py:139 part/templates/part/part_base.html:105 +#: part/bom.py:139 part/templates/part/part_base.html:108 msgid "Available Stock" msgstr "Verfügbarer Lagerbestand" @@ -1974,11 +1985,11 @@ msgstr "Fehler beim Lesen der Stückliste (ungültige Daten)" msgid "Error reading BOM file (incorrect row size)" msgstr "Fehler beim Lesen der Stückliste (ungültige Zeilengröße)" -#: part/forms.py:57 stock/forms.py:250 +#: part/forms.py:57 stock/forms.py:254 msgid "File Format" msgstr "Dateiformat" -#: part/forms.py:57 stock/forms.py:250 +#: part/forms.py:57 stock/forms.py:254 msgid "Select output file format" msgstr "Ausgabe-Dateiformat auswählen" @@ -2068,11 +2079,11 @@ msgstr "Parameter" msgid "Confirm part creation" msgstr "Erstellen des Teils bestätigen" -#: part/forms.py:247 +#: part/forms.py:248 msgid "Input quantity for price calculation" msgstr "Eintragsmenge zur Preisberechnung" -#: part/forms.py:250 +#: part/forms.py:251 msgid "Select currency for price calculation" msgstr "Währung zur Preisberechnung wählen" @@ -2088,131 +2099,131 @@ msgstr "Standard-Stichworte für Teile dieser Kategorie" msgid "Part Category" msgstr "Teilkategorie" -#: part/models.py:76 part/templates/part/category.html:13 -#: part/templates/part/category.html:78 templates/stats.html:12 +#: part/models.py:76 part/templates/part/category.html:18 +#: part/templates/part/category.html:83 templates/stats.html:12 msgid "Part Categories" msgstr "Teile-Kategorien" -#: part/models.py:293 part/models.py:303 +#: part/models.py:345 part/models.py:355 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "Teil '{p1}' wird in Stückliste für Teil '{p2}' benutzt (rekursiv)" -#: part/models.py:383 +#: part/models.py:435 #, fuzzy #| msgid "No serial numbers found" msgid "Next available serial numbers are" msgstr "Keine Seriennummern gefunden" -#: part/models.py:387 +#: part/models.py:439 msgid "Next available serial number is" msgstr "" -#: part/models.py:392 +#: part/models.py:444 #, fuzzy #| msgid "Empty serial number string" msgid "Most recent serial number is" msgstr "Keine Seriennummer angegeben" -#: part/models.py:470 +#: part/models.py:522 msgid "Part must be unique for name, IPN and revision" msgstr "Namen, Teile- und Revisionsnummern müssen eindeutig sein" -#: part/models.py:485 part/templates/part/detail.html:19 +#: part/models.py:537 part/templates/part/detail.html:19 msgid "Part name" msgstr "Name des Teils" -#: part/models.py:489 +#: part/models.py:541 msgid "Is this part a template part?" msgstr "Ist dieses Teil eine Vorlage?" -#: part/models.py:498 +#: part/models.py:550 msgid "Is this part a variant of another part?" msgstr "Ist dieses Teil eine Variante eines anderen Teils?" -#: part/models.py:500 +#: part/models.py:552 msgid "Part description" msgstr "Beschreibung des Teils" -#: part/models.py:502 +#: part/models.py:554 msgid "Part keywords to improve visibility in search results" msgstr "Schlüsselworte um die Sichtbarkeit in Suchergebnissen zu verbessern" -#: part/models.py:507 +#: part/models.py:559 msgid "Part category" msgstr "Teile-Kategorie" -#: part/models.py:509 +#: part/models.py:561 msgid "Internal Part Number" msgstr "Interne Teilenummer" -#: part/models.py:511 +#: part/models.py:563 msgid "Part revision or version number" msgstr "Revisions- oder Versionsnummer" -#: part/models.py:513 +#: part/models.py:565 msgid "Link to extenal URL" msgstr "Link zu einer Externen URL" -#: part/models.py:525 +#: part/models.py:577 msgid "Where is this item normally stored?" msgstr "Wo wird dieses Teil normalerweise gelagert?" -#: part/models.py:569 +#: part/models.py:621 msgid "Default supplier part" msgstr "Standard-Zulieferer?" -#: part/models.py:572 +#: part/models.py:624 msgid "Minimum allowed stock level" msgstr "Minimal zulässiger Lagerbestand" -#: part/models.py:574 +#: part/models.py:626 msgid "Stock keeping units for this part" msgstr "Stock Keeping Units (SKU) für dieses Teil" -#: part/models.py:576 +#: part/models.py:628 msgid "Can this part be built from other parts?" msgstr "Kann dieses Teil aus anderen Teilen angefertigt werden?" -#: part/models.py:578 +#: part/models.py:630 msgid "Can this part be used to build other parts?" msgstr "Kann dieses Teil zum Bau von anderen genutzt werden?" -#: part/models.py:580 +#: part/models.py:632 msgid "Does this part have tracking for unique items?" msgstr "Hat dieses Teil Tracking für einzelne Objekte?" -#: part/models.py:582 +#: part/models.py:634 msgid "Can this part be purchased from external suppliers?" msgstr "Kann dieses Teil von externen Zulieferern gekauft werden?" -#: part/models.py:584 +#: part/models.py:636 msgid "Can this part be sold to customers?" msgstr "Kann dieses Teil an Kunden verkauft werden?" -#: part/models.py:586 +#: part/models.py:638 msgid "Is this part active?" msgstr "Ist dieses Teil aktiv?" -#: part/models.py:588 +#: part/models.py:640 msgid "Is this a virtual part, such as a software product or license?" msgstr "Ist dieses Teil virtuell, wie zum Beispiel eine Software oder Lizenz?" -#: part/models.py:590 +#: part/models.py:642 msgid "Part notes - supports Markdown formatting" msgstr "Bemerkungen - unterstüzt Markdown-Formatierung" -#: part/models.py:592 +#: part/models.py:644 msgid "Stored BOM checksum" msgstr "Prüfsumme der Stückliste gespeichert" -#: part/models.py:1300 +#: part/models.py:1353 #, fuzzy #| msgid "Stock item cannot be created for a template Part" msgid "Test templates can only be created for trackable parts" msgstr "Lagerobjekt kann nicht für Vorlagen-Teile angelegt werden" -#: part/models.py:1317 +#: part/models.py:1370 #, fuzzy #| msgid "" #| "A stock item with this serial number already exists for template part " @@ -2222,114 +2233,120 @@ msgstr "" "Ein Teil mit dieser Seriennummer existiert bereits für die Teilevorlage " "{part}" -#: part/models.py:1336 templates/js/part.html:455 templates/js/stock.html:92 +#: part/models.py:1389 templates/js/part.html:521 templates/js/stock.html:92 #, fuzzy #| msgid "Instance Name" msgid "Test Name" msgstr "Instanzname" -#: part/models.py:1337 +#: part/models.py:1390 #, fuzzy #| msgid "Serial number for this item" msgid "Enter a name for the test" msgstr "Seriennummer für dieses Teil" -#: part/models.py:1342 +#: part/models.py:1395 #, fuzzy #| msgid "Description" msgid "Test Description" msgstr "Beschreibung" -#: part/models.py:1343 +#: part/models.py:1396 #, fuzzy #| msgid "Brief description of the build" msgid "Enter description for this test" msgstr "Kurze Beschreibung des Baus" -#: part/models.py:1349 +#: part/models.py:1402 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1354 templates/js/part.html:472 +#: part/models.py:1407 templates/js/part.html:538 #, fuzzy #| msgid "Required Parts" msgid "Requires Value" msgstr "benötigte Teile" -#: part/models.py:1355 +#: part/models.py:1408 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1360 templates/js/part.html:479 +#: part/models.py:1413 templates/js/part.html:545 #, fuzzy #| msgid "Delete Attachment" msgid "Requires Attachment" msgstr "Anhang löschen" -#: part/models.py:1361 +#: part/models.py:1414 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1394 +#: part/models.py:1447 msgid "Parameter template name must be unique" msgstr "Vorlagen-Name des Parameters muss eindeutig sein" -#: part/models.py:1399 +#: part/models.py:1452 msgid "Parameter Name" msgstr "Name des Parameters" -#: part/models.py:1401 +#: part/models.py:1454 msgid "Parameter Units" msgstr "Parameter Einheit" -#: part/models.py:1427 +#: part/models.py:1480 msgid "Parent Part" msgstr "Ausgangsteil" -#: part/models.py:1429 +#: part/models.py:1482 msgid "Parameter Template" msgstr "Parameter Vorlage" -#: part/models.py:1431 +#: part/models.py:1484 msgid "Parameter Value" msgstr "Parameter Wert" -#: part/models.py:1467 +#: part/models.py:1521 msgid "Select parent part" msgstr "Ausgangsteil auswählen" -#: part/models.py:1475 +#: part/models.py:1529 msgid "Select part to be used in BOM" msgstr "Teil für die Nutzung in der Stückliste auswählen" -#: part/models.py:1481 +#: part/models.py:1535 msgid "BOM quantity for this BOM item" msgstr "Stücklisten-Anzahl für dieses Stücklisten-Teil" -#: part/models.py:1484 +#: part/models.py:1537 +#, fuzzy +#| msgid "Confim BOM item deletion" +msgid "This BOM item is optional" +msgstr "Löschung von BOM-Position bestätigen" + +#: part/models.py:1540 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "Geschätzter Ausschuss (absolut oder prozentual)" -#: part/models.py:1487 +#: part/models.py:1543 msgid "BOM item reference" msgstr "Referenz des Objekts auf der Stückliste" -#: part/models.py:1490 +#: part/models.py:1546 msgid "BOM item notes" msgstr "Notizen zum Stücklisten-Objekt" -#: part/models.py:1492 +#: part/models.py:1548 msgid "BOM line checksum" msgstr "Prüfsumme der Stückliste" -#: part/models.py:1556 part/views.py:1310 part/views.py:1362 -#: stock/models.py:229 +#: part/models.py:1612 part/views.py:1310 part/views.py:1362 +#: stock/models.py:231 #, fuzzy #| msgid "Overage must be an integer value or a percentage" msgid "Quantity must be integer value for trackable parts" msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" -#: part/models.py:1565 +#: part/models.py:1621 #, fuzzy #| msgid "New BOM Item" msgid "BOM Item" @@ -2350,14 +2367,14 @@ msgstr "Bestellung" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:58 -#: stock/templates/stock/item_base.html:226 +#: stock/templates/stock/item_base.html:238 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.html:112 -#: templates/js/stock.html:651 +#: templates/js/stock.html:660 templates/js/stock.html:896 msgid "Stock Item" msgstr "Lagerobjekt" #: part/templates/part/allocation.html:20 -#: stock/templates/stock/item_base.html:180 +#: stock/templates/stock/item_base.html:192 msgid "Build Order" msgstr "Bauauftrag" @@ -2493,109 +2510,115 @@ msgstr "Neues Bild hochladen" msgid "Each part must already exist in the database" msgstr "" -#: part/templates/part/category.html:14 +#: part/templates/part/category.html:19 msgid "All parts" msgstr "Alle Teile" -#: part/templates/part/category.html:18 part/views.py:1935 +#: part/templates/part/category.html:23 part/views.py:1976 msgid "Create new part category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:22 +#: part/templates/part/category.html:27 #, fuzzy #| msgid "Edit Part Category" msgid "Edit part category" msgstr "Teilkategorie bearbeiten" -#: part/templates/part/category.html:25 +#: part/templates/part/category.html:30 #, fuzzy #| msgid "Select part category" msgid "Delete part category" msgstr "Teilekategorie wählen" -#: part/templates/part/category.html:34 part/templates/part/category.html:73 +#: part/templates/part/category.html:39 part/templates/part/category.html:78 msgid "Category Details" msgstr "Kategorie-Details" -#: part/templates/part/category.html:39 +#: part/templates/part/category.html:44 msgid "Category Path" msgstr "Pfad zur Kategorie" -#: part/templates/part/category.html:44 +#: part/templates/part/category.html:49 msgid "Category Description" msgstr "Kategorie-Beschreibung" -#: part/templates/part/category.html:57 part/templates/part/detail.html:64 +#: part/templates/part/category.html:62 part/templates/part/detail.html:64 msgid "Keywords" msgstr "Schlüsselwörter" -#: part/templates/part/category.html:63 +#: part/templates/part/category.html:68 msgid "Subcategories" msgstr "Unter-Kategorien" -#: part/templates/part/category.html:68 +#: part/templates/part/category.html:73 msgid "Parts (Including subcategories)" msgstr "Teile (inklusive Unter-Kategorien)" -#: part/templates/part/category.html:101 +#: part/templates/part/category.html:106 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:102 part/views.py:491 +#: part/templates/part/category.html:107 part/views.py:491 msgid "Create new part" msgstr "Neues Teil anlegen" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:111 #, fuzzy #| msgid "Part category" msgid "Set category" msgstr "Teile-Kategorie" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:111 #, fuzzy #| msgid "Set Part Category" msgid "Set Category" msgstr "Teilkategorie auswählen" -#: part/templates/part/category.html:108 +#: part/templates/part/category.html:113 #, fuzzy #| msgid "Export" msgid "Export Data" msgstr "Exportieren" -#: part/templates/part/category.html:154 +#: part/templates/part/category.html:162 #, fuzzy #| msgid "Create New Location" msgid "Create new location" msgstr "Neuen Standort anlegen" -#: part/templates/part/category.html:159 part/templates/part/category.html:188 +#: part/templates/part/category.html:167 part/templates/part/category.html:196 #, fuzzy #| msgid "Category" msgid "New Category" msgstr "Kategorie" -#: part/templates/part/category.html:160 +#: part/templates/part/category.html:168 #, fuzzy #| msgid "Create new part category" msgid "Create new category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:189 +#: part/templates/part/category.html:197 #, fuzzy #| msgid "Create new part category" msgid "Create new Part Category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:195 stock/views.py:1213 +#: part/templates/part/category.html:203 stock/views.py:1314 msgid "Create new Stock Location" msgstr "Neuen Lager-Standort erstellen" +#: part/templates/part/category_tabs.html:9 +#, fuzzy +#| msgid "Parameter Value" +msgid "Parametric Table" +msgstr "Parameter Wert" + #: part/templates/part/detail.html:9 msgid "Part Details" msgstr "Teile-Details" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:82 +#: part/templates/part/detail.html:25 part/templates/part/part_base.html:85 #: templates/js/part.html:112 msgid "IPN" msgstr "IPN (Interne Produktnummer)" @@ -2621,7 +2644,7 @@ msgid "Variant Of" msgstr "Variante von" #: part/templates/part/detail.html:70 part/templates/part/set_category.html:15 -#: templates/js/part.html:293 +#: templates/js/part.html:359 msgid "Category" msgstr "Kategorie" @@ -2661,8 +2684,8 @@ msgstr "Teil ist virtuell (kein physisches Teil)" msgid "Part is not a virtual part" msgstr "Teil ist nicht virtuell" -#: part/templates/part/detail.html:145 stock/forms.py:244 -#: templates/js/table_filters.html:183 +#: part/templates/part/detail.html:145 stock/forms.py:248 +#: templates/js/table_filters.html:188 msgid "Template" msgstr "Vorlage" @@ -2678,7 +2701,7 @@ msgstr "Teil kann keine Vorlage sein wenn es Variante eines anderen Teils ist" msgid "Part is not a template part" msgstr "Teil ist nicht virtuell" -#: part/templates/part/detail.html:154 templates/js/table_filters.html:195 +#: part/templates/part/detail.html:154 templates/js/table_filters.html:200 msgid "Assembly" msgstr "Baugruppe" @@ -2690,7 +2713,7 @@ msgstr "Teil kann aus anderen Teilen angefertigt werden" msgid "Part cannot be assembled from other parts" msgstr "Teil kann nicht aus anderen Teilen angefertigt werden" -#: part/templates/part/detail.html:163 templates/js/table_filters.html:199 +#: part/templates/part/detail.html:163 templates/js/table_filters.html:204 msgid "Component" msgstr "Komponente" @@ -2702,7 +2725,7 @@ msgstr "Teil kann in Baugruppen benutzt werden" msgid "Part cannot be used in assemblies" msgstr "Teil kann nicht in Baugruppen benutzt werden" -#: part/templates/part/detail.html:172 templates/js/table_filters.html:211 +#: part/templates/part/detail.html:172 templates/js/table_filters.html:216 msgid "Trackable" msgstr "nachverfolgbar" @@ -2722,7 +2745,7 @@ msgstr "Kaufbar" msgid "Part can be purchased from external suppliers" msgstr "Teil kann von externen Zulieferern gekauft werden" -#: part/templates/part/detail.html:190 templates/js/table_filters.html:207 +#: part/templates/part/detail.html:190 templates/js/table_filters.html:212 msgid "Salable" msgstr "Verkäuflich" @@ -2734,7 +2757,7 @@ msgstr "Teil kann an Kunden verkauft werden" msgid "Part cannot be sold to customers" msgstr "Teil kann nicht an Kunden verkauft werden" -#: part/templates/part/detail.html:199 templates/js/table_filters.html:178 +#: part/templates/part/detail.html:199 templates/js/table_filters.html:183 msgid "Active" msgstr "Aktiv" @@ -2770,7 +2793,7 @@ msgstr "Parameter hinzufügen" msgid "New Parameter" msgstr "Neuer Parameter" -#: part/templates/part/params.html:21 stock/models.py:1340 +#: part/templates/part/params.html:21 stock/models.py:1391 #: templates/js/stock.html:112 msgid "Value" msgstr "Wert" @@ -2801,82 +2824,82 @@ msgstr "Dieses Teil ist eine Vorlage." msgid "This part is a variant of" msgstr "Dieses Teil ist eine Variante von" -#: part/templates/part/part_base.html:33 templates/js/company.html:153 -#: templates/js/part.html:270 +#: part/templates/part/part_base.html:36 templates/js/company.html:153 +#: templates/js/part.html:336 msgid "Inactive" msgstr "Inaktiv" -#: part/templates/part/part_base.html:40 +#: part/templates/part/part_base.html:43 msgid "Star this part" msgstr "Teil favorisieren" -#: part/templates/part/part_base.html:46 -#: stock/templates/stock/item_base.html:78 -#: stock/templates/stock/location.html:22 +#: part/templates/part/part_base.html:49 +#: stock/templates/stock/item_base.html:81 +#: stock/templates/stock/location.html:27 #, fuzzy #| msgid "Source Location" msgid "Barcode actions" msgstr "Quell-Standort" -#: part/templates/part/part_base.html:48 -#: stock/templates/stock/item_base.html:80 -#: stock/templates/stock/location.html:24 +#: part/templates/part/part_base.html:51 +#: stock/templates/stock/item_base.html:83 +#: stock/templates/stock/location.html:29 #, fuzzy #| msgid "Part QR Code" msgid "Show QR Code" msgstr "Teil-QR-Code" -#: part/templates/part/part_base.html:49 -#: stock/templates/stock/item_base.html:81 -#: stock/templates/stock/location.html:25 +#: part/templates/part/part_base.html:52 +#: stock/templates/stock/item_base.html:84 +#: stock/templates/stock/location.html:30 msgid "Print Label" msgstr "" -#: part/templates/part/part_base.html:53 +#: part/templates/part/part_base.html:56 msgid "Show pricing information" msgstr "Kosteninformationen ansehen" -#: part/templates/part/part_base.html:67 +#: part/templates/part/part_base.html:70 #, fuzzy #| msgid "Source Location" msgid "Part actions" msgstr "Quell-Standort" -#: part/templates/part/part_base.html:69 +#: part/templates/part/part_base.html:72 #, fuzzy #| msgid "Duplicate Part" msgid "Duplicate part" msgstr "Teil duplizieren" -#: part/templates/part/part_base.html:70 +#: part/templates/part/part_base.html:73 #, fuzzy #| msgid "Edit Template" msgid "Edit part" msgstr "Vorlage bearbeiten" -#: part/templates/part/part_base.html:72 +#: part/templates/part/part_base.html:75 #, fuzzy #| msgid "Delete Parts" msgid "Delete part" msgstr "Teile löschen" -#: part/templates/part/part_base.html:111 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:114 templates/js/table_filters.html:65 msgid "In Stock" msgstr "Auf Lager" -#: part/templates/part/part_base.html:118 +#: part/templates/part/part_base.html:121 msgid "Allocated to Build Orders" msgstr "Zu Bauaufträgen zugeordnet" -#: part/templates/part/part_base.html:125 +#: part/templates/part/part_base.html:128 msgid "Allocated to Sales Orders" msgstr "Zu Aufträgen zugeordnet" -#: part/templates/part/part_base.html:147 +#: part/templates/part/part_base.html:150 msgid "Can Build" msgstr "Herstellbar?" -#: part/templates/part/part_base.html:153 +#: part/templates/part/part_base.html:156 msgid "Underway" msgstr "unterwegs" @@ -2926,8 +2949,8 @@ msgstr "Teil entfernen" msgid "Part Stock" msgstr "Teilbestand" -#: part/templates/part/stock_count.html:7 templates/js/bom.html:193 -#: templates/js/part.html:330 +#: part/templates/part/stock_count.html:7 templates/js/bom.html:197 +#: templates/js/part.html:396 msgid "No Stock" msgstr "Kein Bestand" @@ -2975,7 +2998,7 @@ msgstr "Stückliste" msgid "Used In" msgstr "Benutzt in" -#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:270 +#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:282 msgid "Tests" msgstr "" @@ -3166,27 +3189,27 @@ msgstr "Teilparameter bearbeiten" msgid "Delete Part Parameter" msgstr "Teilparameter löschen" -#: part/views.py:1886 +#: part/views.py:1927 msgid "Edit Part Category" msgstr "Teilkategorie bearbeiten" -#: part/views.py:1921 +#: part/views.py:1962 msgid "Delete Part Category" msgstr "Teilkategorie löschen" -#: part/views.py:1927 +#: part/views.py:1968 msgid "Part category was deleted" msgstr "Teilekategorie wurde gelöscht" -#: part/views.py:1986 +#: part/views.py:2027 msgid "Create BOM item" msgstr "BOM-Position anlegen" -#: part/views.py:2052 +#: part/views.py:2093 msgid "Edit BOM item" msgstr "BOM-Position beaarbeiten" -#: part/views.py:2100 +#: part/views.py:2141 msgid "Confim BOM item deletion" msgstr "Löschung von BOM-Position bestätigen" @@ -3226,308 +3249,346 @@ msgstr "" msgid "Asset file description" msgstr "Einstellungs-Beschreibung" -#: stock/forms.py:187 +#: stock/forms.py:191 msgid "Label" msgstr "" -#: stock/forms.py:188 stock/forms.py:244 +#: stock/forms.py:192 stock/forms.py:248 #, fuzzy #| msgid "Select stock item to allocate" msgid "Select test report template" msgstr "Lagerobjekt für Zuordnung auswählen" -#: stock/forms.py:252 +#: stock/forms.py:256 msgid "Include stock items in sub locations" msgstr "Lagerobjekte in untergeordneten Lagerorten einschließen" -#: stock/forms.py:279 +#: stock/forms.py:291 +#, fuzzy +#| msgid "No stock items matching query" +msgid "Stock item to install" +msgstr "Keine zur Anfrage passenden Lagerobjekte" + +#: stock/forms.py:298 +#, fuzzy +#| msgid "Stock Quantity" +msgid "Stock quantity to assign" +msgstr "Bestand" + +#: stock/forms.py:326 +#, fuzzy +#| msgid "Quantity must not exceed available stock quantity ({n})" +msgid "Must not exceed available quantity" +msgstr "Anzahl darf nicht die verfügbare Anzahl überschreiten ({n})" + +#: stock/forms.py:336 #, fuzzy #| msgid "Does this part have tracking for unique items?" msgid "Destination location for uninstalled items" msgstr "Hat dieses Teil Tracking für einzelne Objekte?" -#: stock/forms.py:281 +#: stock/forms.py:338 #, fuzzy #| msgid "Description of the company" msgid "Add transaction note (optional)" msgstr "Firmenbeschreibung" -#: stock/forms.py:283 +#: stock/forms.py:340 #, fuzzy #| msgid "Confirm stock allocation" msgid "Confirm uninstall" msgstr "Lagerbestandszuordnung bestätigen" -#: stock/forms.py:283 +#: stock/forms.py:340 #, fuzzy #| msgid "Confirm movement of stock items" msgid "Confirm removal of installed stock items" msgstr "Bewegung der Lagerobjekte bestätigen" -#: stock/forms.py:307 +#: stock/forms.py:364 #, fuzzy #| msgid "Description" msgid "Destination" msgstr "Beschreibung" -#: stock/forms.py:307 +#: stock/forms.py:364 msgid "Destination stock location" msgstr "Ziel-Lagerbestand" -#: stock/forms.py:309 +#: stock/forms.py:366 msgid "Add note (required)" msgstr "" -#: stock/forms.py:313 stock/views.py:795 stock/views.py:992 +#: stock/forms.py:370 stock/views.py:895 stock/views.py:1092 msgid "Confirm stock adjustment" msgstr "Bestands-Anpassung bestätigen" -#: stock/forms.py:313 +#: stock/forms.py:370 msgid "Confirm movement of stock items" msgstr "Bewegung der Lagerobjekte bestätigen" -#: stock/forms.py:315 +#: stock/forms.py:372 #, fuzzy #| msgid "Default Location" msgid "Set Default Location" msgstr "Standard-Lagerort" -#: stock/forms.py:315 +#: stock/forms.py:372 msgid "Set the destination as the default location for selected parts" msgstr "Setze das Ziel als Standard-Ziel für ausgewählte Teile" -#: stock/models.py:210 +#: stock/models.py:212 #, fuzzy #| msgid "A stock item with this serial number already exists" msgid "StockItem with this serial number already exists" msgstr "Ein Teil mit dieser Seriennummer existiert bereits" -#: stock/models.py:246 +#: stock/models.py:248 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "Teile-Typ ('{pf}') muss {pe} sein" -#: stock/models.py:256 stock/models.py:265 +#: stock/models.py:258 stock/models.py:267 msgid "Quantity must be 1 for item with a serial number" msgstr "Anzahl muss für Objekte mit Seriennummer \"1\" sein" -#: stock/models.py:257 +#: stock/models.py:259 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" "Seriennummer kann nicht gesetzt werden wenn die Anzahl größer als \"1\" ist" -#: stock/models.py:278 +#: stock/models.py:281 msgid "Item cannot belong to itself" msgstr "Teil kann nicht zu sich selbst gehören" -#: stock/models.py:311 +#: stock/models.py:287 +msgid "Item must have a build reference if is_building=True" +msgstr "" + +#: stock/models.py:294 +msgid "Build reference does not point to the same part object" +msgstr "" + +#: stock/models.py:327 msgid "Parent Stock Item" msgstr "Eltern-Lagerobjekt" -#: stock/models.py:320 +#: stock/models.py:336 msgid "Base part" msgstr "Basis-Teil" -#: stock/models.py:329 +#: stock/models.py:345 msgid "Select a matching supplier part for this stock item" msgstr "Passenden Zulieferer für dieses Lagerobjekt auswählen" -#: stock/models.py:334 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:350 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "Lagerort" -#: stock/models.py:337 +#: stock/models.py:353 msgid "Where is this stock item located?" msgstr "Wo wird dieses Teil normalerweise gelagert?" -#: stock/models.py:342 +#: stock/models.py:358 stock/templates/stock/item_base.html:177 msgid "Installed In" msgstr "Installiert in" -#: stock/models.py:345 +#: stock/models.py:361 msgid "Is this item installed in another item?" msgstr "Ist dieses Teil in einem anderen verbaut?" -#: stock/models.py:361 +#: stock/models.py:377 msgid "Serial number for this item" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:373 +#: stock/models.py:389 msgid "Batch code for this stock item" msgstr "Losnummer für dieses Lagerobjekt" -#: stock/models.py:377 +#: stock/models.py:393 msgid "Stock Quantity" msgstr "Bestand" -#: stock/models.py:386 +#: stock/models.py:402 msgid "Source Build" msgstr "Quellbau" -#: stock/models.py:388 +#: stock/models.py:404 msgid "Build for this stock item" msgstr "Bau für dieses Lagerobjekt" -#: stock/models.py:395 +#: stock/models.py:415 msgid "Source Purchase Order" msgstr "Quellbestellung" -#: stock/models.py:398 +#: stock/models.py:418 msgid "Purchase order for this stock item" msgstr "Bestellung für dieses Teil" -#: stock/models.py:404 +#: stock/models.py:424 msgid "Destination Sales Order" msgstr "Zielauftrag" -#: stock/models.py:411 +#: stock/models.py:431 msgid "Destination Build Order" msgstr "Zielbauauftrag" -#: stock/models.py:424 +#: stock/models.py:444 msgid "Delete this Stock Item when stock is depleted" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" -#: stock/models.py:434 stock/templates/stock/item_notes.html:14 +#: stock/models.py:454 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "Lagerobjekt-Notizen" -#: stock/models.py:485 +#: stock/models.py:505 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assigned to Customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:487 +#: stock/models.py:507 #, fuzzy #| msgid "Item assigned to customer?" msgid "Manually assigned to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:500 +#: stock/models.py:520 #, fuzzy #| msgid "Item assigned to customer?" msgid "Returned from customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:502 +#: stock/models.py:522 #, fuzzy #| msgid "Create new stock location" msgid "Returned to location" msgstr "Neuen Lagerort anlegen" -#: stock/models.py:626 +#: stock/models.py:650 #, fuzzy #| msgid "Installed in Stock Item" -msgid "Installed in stock item" +msgid "Installed into stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:655 +#: stock/models.py:658 +#, fuzzy +#| msgid "Installed in Stock Item" +msgid "Installed stock item" +msgstr "In Lagerobjekt installiert" + +#: stock/models.py:682 +#, fuzzy +#| msgid "Installed in Stock Item" +msgid "Uninstalled stock item" +msgstr "In Lagerobjekt installiert" + +#: stock/models.py:701 #, fuzzy #| msgid "Include sublocations" msgid "Uninstalled into location" msgstr "Unterlagerorte einschließen" -#: stock/models.py:745 +#: stock/models.py:796 #, fuzzy #| msgid "Part is not a virtual part" msgid "Part is not set as trackable" msgstr "Teil ist nicht virtuell" -#: stock/models.py:751 +#: stock/models.py:802 msgid "Quantity must be integer" msgstr "Anzahl muss eine Ganzzahl sein" -#: stock/models.py:757 +#: stock/models.py:808 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "Anzahl darf nicht die verfügbare Anzahl überschreiten ({n})" -#: stock/models.py:760 +#: stock/models.py:811 msgid "Serial numbers must be a list of integers" msgstr "Seriennummern muss eine Liste von Ganzzahlen sein" -#: stock/models.py:763 +#: stock/models.py:814 msgid "Quantity does not match serial numbers" msgstr "Anzahl stimmt nicht mit den Seriennummern überein" -#: stock/models.py:773 +#: stock/models.py:824 msgid "Serial numbers already exist: " msgstr "Seriennummern existieren bereits:" -#: stock/models.py:798 +#: stock/models.py:849 msgid "Add serial number" msgstr "Seriennummer hinzufügen" -#: stock/models.py:801 +#: stock/models.py:852 #, python-brace-format msgid "Serialized {n} items" msgstr "{n} Teile serialisiert" -#: stock/models.py:912 +#: stock/models.py:963 msgid "StockItem cannot be moved as it is not in stock" msgstr "Lagerobjekt kann nicht bewegt werden, da kein Bestand vorhanden ist" -#: stock/models.py:1241 +#: stock/models.py:1292 msgid "Tracking entry title" msgstr "Name des Eintrags-Trackings" -#: stock/models.py:1243 +#: stock/models.py:1294 msgid "Entry notes" msgstr "Eintrags-Notizen" -#: stock/models.py:1245 +#: stock/models.py:1296 msgid "Link to external page for further information" msgstr "Link auf externe Seite für weitere Informationen" -#: stock/models.py:1305 +#: stock/models.py:1356 #, fuzzy #| msgid "Serial number for this item" msgid "Value must be provided for this test" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:1311 +#: stock/models.py:1362 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1328 +#: stock/models.py:1379 msgid "Test" msgstr "" -#: stock/models.py:1329 +#: stock/models.py:1380 #, fuzzy #| msgid "Part name" msgid "Test name" msgstr "Name des Teils" -#: stock/models.py:1334 +#: stock/models.py:1385 #, fuzzy #| msgid "Search Results" msgid "Result" msgstr "Suchergebnisse" -#: stock/models.py:1335 templates/js/table_filters.html:111 +#: stock/models.py:1386 templates/js/table_filters.html:111 msgid "Test result" msgstr "" -#: stock/models.py:1341 +#: stock/models.py:1392 msgid "Test output value" msgstr "" -#: stock/models.py:1347 +#: stock/models.py:1398 #, fuzzy #| msgid "Attachments" msgid "Attachment" msgstr "Anhänge" -#: stock/models.py:1348 +#: stock/models.py:1399 #, fuzzy #| msgid "Delete attachment" msgid "Test result attachment" msgstr "Anhang löschen" -#: stock/models.py:1354 +#: stock/models.py:1405 #, fuzzy #| msgid "Edit notes" msgid "Test notes" @@ -3576,124 +3637,130 @@ msgstr "" "Dieses Lagerobjekt wird automatisch gelöscht wenn der Lagerbestand " "aufgebraucht ist." -#: stock/templates/stock/item_base.html:83 templates/js/barcode.html:283 +#: stock/templates/stock/item_base.html:86 templates/js/barcode.html:283 #: templates/js/barcode.html:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:85 +#: stock/templates/stock/item_base.html:88 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:91 +#: stock/templates/stock/item_base.html:94 #, fuzzy #| msgid "Confirm stock adjustment" msgid "Stock adjustment actions" msgstr "Bestands-Anpassung bestätigen" -#: stock/templates/stock/item_base.html:95 -#: stock/templates/stock/location.html:33 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:98 +#: stock/templates/stock/location.html:38 templates/stock_table.html:15 msgid "Count stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:96 templates/stock_table.html:12 +#: stock/templates/stock/item_base.html:99 templates/stock_table.html:13 msgid "Add stock" msgstr "Bestand hinzufügen" -#: stock/templates/stock/item_base.html:97 templates/stock_table.html:13 +#: stock/templates/stock/item_base.html:100 templates/stock_table.html:14 msgid "Remove stock" msgstr "Bestand entfernen" -#: stock/templates/stock/item_base.html:99 +#: stock/templates/stock/item_base.html:102 #, fuzzy #| msgid "Order stock" msgid "Transfer stock" msgstr "Bestand bestellen" -#: stock/templates/stock/item_base.html:101 +#: stock/templates/stock/item_base.html:104 #, fuzzy #| msgid "Serialize Stock" msgid "Serialize stock" msgstr "Lagerbestand erfassen" -#: stock/templates/stock/item_base.html:105 +#: stock/templates/stock/item_base.html:108 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assign to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/templates/stock/item_base.html:108 +#: stock/templates/stock/item_base.html:111 #, fuzzy #| msgid "Count stock" msgid "Return to stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:114 -#: stock/templates/stock/location.html:30 +#: stock/templates/stock/item_base.html:115 templates/js/stock.html:933 +#, fuzzy +#| msgid "Installed in Stock Item" +msgid "Uninstall stock item" +msgstr "In Lagerobjekt installiert" + +#: stock/templates/stock/item_base.html:115 +msgid "Uninstall" +msgstr "" + +#: stock/templates/stock/item_base.html:122 +#: stock/templates/stock/location.html:35 #, fuzzy #| msgid "Stock Locations" msgid "Stock actions" msgstr "Lagerobjekt-Standorte" -#: stock/templates/stock/item_base.html:118 +#: stock/templates/stock/item_base.html:126 #, fuzzy #| msgid "Count stock items" msgid "Convert to variant" msgstr "Lagerobjekte zählen" -#: stock/templates/stock/item_base.html:120 +#: stock/templates/stock/item_base.html:128 #, fuzzy #| msgid "Count stock items" msgid "Duplicate stock item" msgstr "Lagerobjekte zählen" -#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/item_base.html:129 #, fuzzy #| msgid "Edit Stock Item" msgid "Edit stock item" msgstr "Lagerobjekt bearbeiten" -#: stock/templates/stock/item_base.html:123 +#: stock/templates/stock/item_base.html:131 #, fuzzy #| msgid "Delete Stock Item" msgid "Delete stock item" msgstr "Lagerobjekt löschen" -#: stock/templates/stock/item_base.html:127 +#: stock/templates/stock/item_base.html:135 msgid "Generate test report" msgstr "" -#: stock/templates/stock/item_base.html:135 +#: stock/templates/stock/item_base.html:143 msgid "Stock Item Details" msgstr "Lagerbestands-Details" -#: stock/templates/stock/item_base.html:168 -msgid "Belongs To" -msgstr "Gehört zu" - -#: stock/templates/stock/item_base.html:190 +#: stock/templates/stock/item_base.html:202 #, fuzzy #| msgid "No stock location set" msgid "No location set" msgstr "Kein Lagerort gesetzt" -#: stock/templates/stock/item_base.html:197 +#: stock/templates/stock/item_base.html:209 msgid "Unique Identifier" msgstr "Eindeutiger Bezeichner" -#: stock/templates/stock/item_base.html:225 +#: stock/templates/stock/item_base.html:237 msgid "Parent Item" msgstr "Elternposition" -#: stock/templates/stock/item_base.html:250 +#: stock/templates/stock/item_base.html:262 msgid "Last Updated" msgstr "Zuletzt aktualisiert" -#: stock/templates/stock/item_base.html:255 +#: stock/templates/stock/item_base.html:267 msgid "Last Stocktake" msgstr "Letzte Inventur" -#: stock/templates/stock/item_base.html:259 +#: stock/templates/stock/item_base.html:271 msgid "No stocktake performed" msgstr "Keine Inventur ausgeführt" @@ -3711,35 +3778,40 @@ msgstr "Dieses Lagerobjekt hat keine Kinder" msgid "Are you sure you want to delete this stock item?" msgstr "Sind Sie sicher, dass Sie diesen Anhang löschen wollen?" +#: stock/templates/stock/item_install.html:7 +#, fuzzy +#| msgid "Installed in Stock Item" +msgid "Install another StockItem into this item." +msgstr "In Lagerobjekt installiert" + +#: stock/templates/stock/item_install.html:10 +msgid "Stock items can only be installed if they meet the following criteria" +msgstr "" + +#: stock/templates/stock/item_install.html:13 +msgid "The StockItem links to a Part which is in the BOM for this StockItem" +msgstr "" + +#: stock/templates/stock/item_install.html:14 +#, fuzzy +#| msgid "This stock item is allocated to Build" +msgid "The StockItem is currently in stock" +msgstr "Dieses Lagerobjekt ist dem Bau zugewiesen" + #: stock/templates/stock/item_installed.html:10 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed Stock Items" msgstr "In Lagerobjekt installiert" -#: stock/templates/stock/item_installed.html:18 +#: stock/templates/stock/item_serialize.html:5 #, fuzzy -#| msgid "Added stock to {n} items" -msgid "Uninstall selected stock items" -msgstr "Vorrat zu {n} Lagerobjekten hinzugefügt" +#| msgid "Purchase order for this stock item" +msgid "Create serialized items from this stock item." +msgstr "Bestellung für dieses Teil" -#: stock/templates/stock/item_installed.html:18 -msgid "Uninstall" -msgstr "" - -#: stock/templates/stock/item_installed.html:35 -#, fuzzy -#| msgid "No stock items matching query" -msgid "No stock items installed" -msgstr "Keine zur Anfrage passenden Lagerobjekte" - -#: stock/templates/stock/item_installed.html:48 templates/js/part.html:209 -#: templates/js/stock.html:409 -msgid "Select" -msgstr "Auswählen" - -#: stock/templates/stock/item_installed.html:131 -msgid "Uninstall item" +#: stock/templates/stock/item_serialize.html:7 +msgid "Select quantity to serialize, and unique serial numbers." msgstr "" #: stock/templates/stock/item_tests.html:10 stock/templates/stock/tabs.html:13 @@ -3760,62 +3832,62 @@ msgstr "" msgid "Test Report" msgstr "" -#: stock/templates/stock/location.html:13 +#: stock/templates/stock/location.html:18 msgid "All stock items" msgstr "Alle Lagerobjekte" -#: stock/templates/stock/location.html:26 +#: stock/templates/stock/location.html:31 #, fuzzy #| msgid "Child Stock Items" msgid "Check-in Items" msgstr "Kind-Lagerobjekte" -#: stock/templates/stock/location.html:37 +#: stock/templates/stock/location.html:42 #, fuzzy #| msgid "Location Description" msgid "Location actions" msgstr "Standort-Beschreibung" -#: stock/templates/stock/location.html:39 +#: stock/templates/stock/location.html:44 #, fuzzy #| msgid "Edit stock location" msgid "Edit location" msgstr "Lagerort bearbeiten" -#: stock/templates/stock/location.html:40 +#: stock/templates/stock/location.html:45 #, fuzzy #| msgid "Delete stock location" msgid "Delete location" msgstr "Lagerort löschen" -#: stock/templates/stock/location.html:48 +#: stock/templates/stock/location.html:53 msgid "Location Details" msgstr "Standort-Details" -#: stock/templates/stock/location.html:53 +#: stock/templates/stock/location.html:58 msgid "Location Path" msgstr "Standord-Pfad" -#: stock/templates/stock/location.html:58 +#: stock/templates/stock/location.html:63 msgid "Location Description" msgstr "Standort-Beschreibung" -#: stock/templates/stock/location.html:63 +#: stock/templates/stock/location.html:68 msgid "Sublocations" msgstr "Sub-Standorte" -#: stock/templates/stock/location.html:68 -#: stock/templates/stock/location.html:83 +#: stock/templates/stock/location.html:73 +#: stock/templates/stock/location.html:88 #: templates/InvenTree/search_stock_items.html:6 templates/stats.html:21 #: templates/stats.html:30 msgid "Stock Items" msgstr "Lagerobjekte" -#: stock/templates/stock/location.html:73 +#: stock/templates/stock/location.html:78 msgid "Stock Details" msgstr "Objekt-Details" -#: stock/templates/stock/location.html:78 +#: stock/templates/stock/location.html:83 #: templates/InvenTree/search_stock_location.html:6 templates/stats.html:25 msgid "Stock Locations" msgstr "Lagerobjekt-Standorte" @@ -3832,7 +3904,7 @@ msgstr "Sind Sie sicher, dass Sie diesen Anhang löschen wollen?" msgid "The following stock items will be uninstalled" msgstr "Die folgenden Objekte werden erstellt" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1186 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1287 #, fuzzy #| msgid "Count Stock Items" msgid "Convert Stock Item" @@ -3978,138 +4050,144 @@ msgstr "Lagerbestandsexportoptionen" msgid "Stock Item QR Code" msgstr "Lagerobjekt-QR-Code" -#: stock/views.py:699 +#: stock/views.py:700 +#, fuzzy +#| msgid "Installed in Stock Item" +msgid "Install Stock Item" +msgstr "In Lagerobjekt installiert" + +#: stock/views.py:799 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstall Stock Items" msgstr "In Lagerobjekt installiert" -#: stock/views.py:806 +#: stock/views.py:906 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstalled stock items" msgstr "In Lagerobjekt installiert" -#: stock/views.py:831 +#: stock/views.py:931 msgid "Adjust Stock" msgstr "Lagerbestand anpassen" -#: stock/views.py:940 +#: stock/views.py:1040 msgid "Move Stock Items" msgstr "Lagerobjekte bewegen" -#: stock/views.py:941 +#: stock/views.py:1041 msgid "Count Stock Items" msgstr "Lagerobjekte zählen" -#: stock/views.py:942 +#: stock/views.py:1042 msgid "Remove From Stock" msgstr "Aus Lagerbestand entfernen" -#: stock/views.py:943 +#: stock/views.py:1043 msgid "Add Stock Items" msgstr "Lagerobjekte hinzufügen" -#: stock/views.py:944 +#: stock/views.py:1044 msgid "Delete Stock Items" msgstr "Lagerobjekte löschen" -#: stock/views.py:972 +#: stock/views.py:1072 msgid "Must enter integer value" msgstr "Nur Ganzzahl eingeben" -#: stock/views.py:977 +#: stock/views.py:1077 msgid "Quantity must be positive" msgstr "Anzahl muss positiv sein" -#: stock/views.py:984 +#: stock/views.py:1084 #, python-brace-format msgid "Quantity must not exceed {x}" msgstr "Anzahl darf {x} nicht überschreiten" -#: stock/views.py:1063 +#: stock/views.py:1163 #, python-brace-format msgid "Added stock to {n} items" msgstr "Vorrat zu {n} Lagerobjekten hinzugefügt" -#: stock/views.py:1078 +#: stock/views.py:1178 #, python-brace-format msgid "Removed stock from {n} items" msgstr "Vorrat von {n} Lagerobjekten entfernt" -#: stock/views.py:1091 +#: stock/views.py:1191 #, python-brace-format msgid "Counted stock for {n} items" msgstr "Bestand für {n} Objekte erfasst" -#: stock/views.py:1119 +#: stock/views.py:1219 msgid "No items were moved" msgstr "Keine Lagerobjekte wurden bewegt" -#: stock/views.py:1122 +#: stock/views.py:1222 #, python-brace-format msgid "Moved {n} items to {dest}" msgstr "{n} Teile nach {dest} bewegt" -#: stock/views.py:1141 +#: stock/views.py:1241 #, python-brace-format msgid "Deleted {n} stock items" msgstr "{n} Teile im Lager gelöscht" -#: stock/views.py:1153 +#: stock/views.py:1253 msgid "Edit Stock Item" msgstr "Lagerobjekt bearbeiten" -#: stock/views.py:1234 +#: stock/views.py:1335 msgid "Serialize Stock" msgstr "Lagerbestand erfassen" -#: stock/views.py:1426 +#: stock/views.py:1527 #, fuzzy #| msgid "Count stock items" msgid "Duplicate Stock Item" msgstr "Lagerobjekte zählen" -#: stock/views.py:1492 +#: stock/views.py:1593 msgid "Invalid quantity" msgstr "Ungültige Menge" -#: stock/views.py:1495 +#: stock/views.py:1596 #, fuzzy #| msgid "Quantity must be greater than zero" msgid "Quantity cannot be less than zero" msgstr "Anzahl muss größer Null sein" -#: stock/views.py:1499 +#: stock/views.py:1600 msgid "Invalid part selection" msgstr "Ungültige Teileauswahl" -#: stock/views.py:1548 +#: stock/views.py:1649 #, python-brace-format msgid "Created {n} new stock items" msgstr "{n} neue Lagerobjekte erstellt" -#: stock/views.py:1567 stock/views.py:1583 +#: stock/views.py:1668 stock/views.py:1684 msgid "Created new stock item" msgstr "Neues Lagerobjekt erstellt" -#: stock/views.py:1602 +#: stock/views.py:1703 msgid "Delete Stock Location" msgstr "Standort löschen" -#: stock/views.py:1615 +#: stock/views.py:1716 msgid "Delete Stock Item" msgstr "Lagerobjekt löschen" -#: stock/views.py:1626 +#: stock/views.py:1727 msgid "Delete Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag löschen" -#: stock/views.py:1643 +#: stock/views.py:1744 msgid "Edit Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag bearbeiten" -#: stock/views.py:1652 +#: stock/views.py:1753 msgid "Add Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag hinzufügen" @@ -4145,17 +4223,25 @@ msgstr "Bau fertigstellen" msgid "Search Results" msgstr "Suchergebnisse" -#: templates/InvenTree/search.html:22 -msgid "No results found" +#: templates/InvenTree/search.html:24 +#, fuzzy +#| msgid "No results found" +msgid "No results found for " msgstr "Keine Ergebnisse gefunden" -#: templates/InvenTree/search.html:181 templates/js/stock.html:521 +#: templates/InvenTree/search.html:42 +#, fuzzy +#| msgid "Cancel sales order" +msgid "Enter a search query" +msgstr "Auftrag stornieren" + +#: templates/InvenTree/search.html:191 templates/js/stock.html:527 #, fuzzy #| msgid "Item assigned to customer?" msgid "Shipped to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: templates/InvenTree/search.html:184 templates/js/stock.html:528 +#: templates/InvenTree/search.html:194 templates/js/stock.html:537 msgid "No stock location set" msgstr "Kein Lagerort gesetzt" @@ -4366,33 +4452,39 @@ msgstr "Neues Lagerobjekt hinzufügen" msgid "Open subassembly" msgstr "Unterbaugruppe öffnen" -#: templates/js/bom.html:184 templates/js/build.html:119 +#: templates/js/bom.html:173 +#, fuzzy +#| msgid "Options" +msgid "Optional" +msgstr "Optionen" + +#: templates/js/bom.html:188 templates/js/build.html:119 msgid "Available" msgstr "verfügbar" -#: templates/js/bom.html:209 +#: templates/js/bom.html:213 msgid "No pricing available" msgstr "Keine Preisinformation verfügbar" -#: templates/js/bom.html:228 +#: templates/js/bom.html:232 #, fuzzy #| msgid "Options" msgid "Actions" msgstr "Optionen" -#: templates/js/bom.html:236 +#: templates/js/bom.html:240 msgid "Validate BOM Item" msgstr "BOM-Position validieren" -#: templates/js/bom.html:238 +#: templates/js/bom.html:242 msgid "This line has been validated" msgstr "Diese Position wurde validiert" -#: templates/js/bom.html:240 +#: templates/js/bom.html:244 msgid "Edit BOM Item" msgstr "BOM-Position bearbeiten" -#: templates/js/bom.html:242 +#: templates/js/bom.html:246 msgid "Delete BOM Item" msgstr "BOM-Position löschen" @@ -4424,11 +4516,11 @@ msgstr "Keine Firmeninformation gefunden" msgid "No supplier parts found" msgstr "Keine Zuliefererteile gefunden" -#: templates/js/company.html:145 templates/js/part.html:248 +#: templates/js/company.html:145 templates/js/part.html:314 msgid "Template part" msgstr "Vorlagenteil" -#: templates/js/company.html:149 templates/js/part.html:252 +#: templates/js/company.html:149 templates/js/part.html:318 msgid "Assembled part" msgstr "Baugruppe" @@ -4440,7 +4532,7 @@ msgstr "Link" msgid "No purchase orders found" msgstr "Keine Bestellungen gefunden" -#: templates/js/order.html:172 templates/js/stock.html:633 +#: templates/js/order.html:172 templates/js/stock.html:642 msgid "Date" msgstr "Datum" @@ -4458,57 +4550,62 @@ msgstr "Versanddatum" msgid "No variants found" msgstr "Keine Teile gefunden" -#: templates/js/part.html:256 -msgid "Starred part" -msgstr "Favoritenteil" - -#: templates/js/part.html:260 -msgid "Salable part" -msgstr "Verkäufliches Teil" - -#: templates/js/part.html:299 -msgid "No category" -msgstr "Keine Kategorie" - -#: templates/js/part.html:317 templates/js/table_filters.html:191 -msgid "Low stock" -msgstr "Bestand niedrig" - -#: templates/js/part.html:326 -msgid "Building" -msgstr "Im Bau" - -#: templates/js/part.html:345 +#: templates/js/part.html:223 templates/js/part.html:411 msgid "No parts found" msgstr "Keine Teile gefunden" -#: templates/js/part.html:405 +#: templates/js/part.html:275 templates/js/stock.html:409 +#: templates/js/stock.html:965 +msgid "Select" +msgstr "Auswählen" + +#: templates/js/part.html:322 +msgid "Starred part" +msgstr "Favoritenteil" + +#: templates/js/part.html:326 +msgid "Salable part" +msgstr "Verkäufliches Teil" + +#: templates/js/part.html:365 +msgid "No category" +msgstr "Keine Kategorie" + +#: templates/js/part.html:383 templates/js/table_filters.html:196 +msgid "Low stock" +msgstr "Bestand niedrig" + +#: templates/js/part.html:392 +msgid "Building" +msgstr "Im Bau" + +#: templates/js/part.html:471 msgid "YES" msgstr "" -#: templates/js/part.html:407 +#: templates/js/part.html:473 msgid "NO" msgstr "" -#: templates/js/part.html:441 +#: templates/js/part.html:507 #, fuzzy #| msgid "No stock items matching query" msgid "No test templates matching query" msgstr "Keine zur Anfrage passenden Lagerobjekte" -#: templates/js/part.html:492 templates/js/stock.html:63 +#: templates/js/part.html:558 templates/js/stock.html:63 #, fuzzy #| msgid "Edit Sales Order" msgid "Edit test result" msgstr "Auftrag bearbeiten" -#: templates/js/part.html:493 templates/js/stock.html:64 +#: templates/js/part.html:559 templates/js/stock.html:64 #, fuzzy #| msgid "Delete attachment" msgid "Delete test result" msgstr "Anhang löschen" -#: templates/js/part.html:499 +#: templates/js/part.html:565 msgid "This test is defined for a parent part" msgstr "" @@ -4564,54 +4661,84 @@ msgstr "Lagerobjekt wurde zugewiesen" msgid "Stock item has been assigned to customer" msgstr "Lagerobjekt wurde zugewiesen" -#: templates/js/stock.html:474 +#: templates/js/stock.html:475 #, fuzzy #| msgid "This stock item is allocated to Sales Order" msgid "Stock item was assigned to a build order" msgstr "Dieses Lagerobjekt ist dem Auftrag zugewiesen" -#: templates/js/stock.html:476 +#: templates/js/stock.html:477 #, fuzzy #| msgid "This stock item is allocated to Sales Order" msgid "Stock item was assigned to a sales order" msgstr "Dieses Lagerobjekt ist dem Auftrag zugewiesen" -#: templates/js/stock.html:483 +#: templates/js/stock.html:482 +#, fuzzy +#| msgid "Is this item installed in another item?" +msgid "Stock item has been installed in another item" +msgstr "Ist dieses Teil in einem anderen verbaut?" + +#: templates/js/stock.html:489 #, fuzzy #| msgid "StockItem has been allocated" msgid "Stock item has been rejected" msgstr "Lagerobjekt wurde zugewiesen" -#: templates/js/stock.html:487 +#: templates/js/stock.html:493 #, fuzzy #| msgid "StockItem is lost" msgid "Stock item is lost" msgstr "Lagerobjekt verloren" -#: templates/js/stock.html:491 templates/js/table_filters.html:60 +#: templates/js/stock.html:497 templates/js/table_filters.html:60 #, fuzzy #| msgid "Delete" msgid "Depleted" msgstr "Löschen" -#: templates/js/stock.html:516 +#: templates/js/stock.html:522 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed in Stock Item " msgstr "In Lagerobjekt installiert" -#: templates/js/stock.html:699 +#: templates/js/stock.html:530 +#, fuzzy +#| msgid "Item assigned to customer?" +msgid "Assigned to sales order" +msgstr "Ist dieses Objekt einem Kunden zugeteilt?" + +#: templates/js/stock.html:708 msgid "No user information" msgstr "Keine Benutzerinformation" -#: templates/js/stock.html:783 +#: templates/js/stock.html:792 msgid "Create New Part" msgstr "Neues Teil anlegen" -#: templates/js/stock.html:795 +#: templates/js/stock.html:804 msgid "Create New Location" msgstr "Neuen Standort anlegen" +#: templates/js/stock.html:903 +#, fuzzy +#| msgid "Serial Number" +msgid "Serial" +msgstr "Seriennummer" + +#: templates/js/stock.html:996 templates/js/table_filters.html:70 +#, fuzzy +#| msgid "Installed In" +msgid "Installed" +msgstr "Installiert in" + +#: templates/js/stock.html:1021 +#, fuzzy +#| msgid "Installed In" +msgid "Install item" +msgstr "Installiert in" + #: templates/js/table_filters.html:19 templates/js/table_filters.html:80 #, fuzzy #| msgid "Serialize Stock" @@ -4689,12 +4816,6 @@ msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" msgid "Show items which are in stock" msgstr "" -#: templates/js/table_filters.html:70 -#, fuzzy -#| msgid "Installed In" -msgid "Installed" -msgstr "Installiert in" - #: templates/js/table_filters.html:71 #, fuzzy #| msgid "Is this item installed in another item?" @@ -4737,19 +4858,29 @@ msgstr "Unterkategorien einschließen" msgid "Include parts in subcategories" msgstr "Teile in Unterkategorien einschließen" +#: templates/js/table_filters.html:178 +msgid "Has IPN" +msgstr "" + #: templates/js/table_filters.html:179 +#, fuzzy +#| msgid "Internal Part Number" +msgid "Part has internal part number" +msgstr "Interne Teilenummer" + +#: templates/js/table_filters.html:184 msgid "Show active parts" msgstr "Aktive Teile anzeigen" -#: templates/js/table_filters.html:187 +#: templates/js/table_filters.html:192 msgid "Stock available" msgstr "Bestand verfügbar" -#: templates/js/table_filters.html:203 +#: templates/js/table_filters.html:208 msgid "Starred" msgstr "Favorit" -#: templates/js/table_filters.html:215 +#: templates/js/table_filters.html:220 msgid "Purchasable" msgstr "Käuflich" @@ -4793,60 +4924,68 @@ msgstr "Statistiken" msgid "Search" msgstr "Suche" -#: templates/stock_table.html:5 +#: templates/stock_table.html:6 #, fuzzy #| msgid "Edit Stock Location" msgid "Export Stock Information" msgstr "Lagerobjekt-Standort bearbeiten" -#: templates/stock_table.html:12 +#: templates/stock_table.html:13 #, fuzzy #| msgid "Added stock to {n} items" msgid "Add to selected stock items" msgstr "Vorrat zu {n} Lagerobjekten hinzugefügt" -#: templates/stock_table.html:13 +#: templates/stock_table.html:14 #, fuzzy #| msgid "Remove selected BOM items" msgid "Remove from selected stock items" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: templates/stock_table.html:14 +#: templates/stock_table.html:15 #, fuzzy #| msgid "Delete Stock Item" msgid "Stocktake selected stock items" msgstr "Lagerobjekt löschen" -#: templates/stock_table.html:15 +#: templates/stock_table.html:16 #, fuzzy #| msgid "Delete Stock Item" msgid "Move selected stock items" msgstr "Lagerobjekt löschen" -#: templates/stock_table.html:15 +#: templates/stock_table.html:16 msgid "Move stock" msgstr "Bestand bewegen" -#: templates/stock_table.html:16 +#: templates/stock_table.html:17 #, fuzzy #| msgid "Remove selected BOM items" msgid "Order selected items" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: templates/stock_table.html:16 +#: templates/stock_table.html:17 msgid "Order stock" msgstr "Bestand bestellen" -#: templates/stock_table.html:17 +#: templates/stock_table.html:18 #, fuzzy #| msgid "Delete line item" msgid "Delete selected items" msgstr "Position löschen" -#: templates/stock_table.html:17 +#: templates/stock_table.html:18 msgid "Delete Stock" msgstr "Bestand löschen" +#~ msgid "Belongs To" +#~ msgstr "Gehört zu" + +#, fuzzy +#~| msgid "Added stock to {n} items" +#~ msgid "Uninstall selected stock items" +#~ msgstr "Vorrat zu {n} Lagerobjekten hinzugefügt" + #~ msgid "Order Multiple" #~ msgstr "Bestellvielfaches" diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index d4a3d7ceb6..0b5425db6e 100644 --- a/InvenTree/locale/en/LC_MESSAGES/django.po +++ b/InvenTree/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-09-28 12:03+0000\n" +"POT-Creation-Date: 2020-10-04 14:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -86,7 +86,7 @@ msgstr "" msgid "File comment" msgstr "" -#: InvenTree/models.py:68 templates/js/stock.html:690 +#: InvenTree/models.py:68 templates/js/stock.html:699 msgid "User" msgstr "" @@ -99,19 +99,19 @@ msgstr "" msgid "Description (optional)" msgstr "" -#: InvenTree/settings.py:341 +#: InvenTree/settings.py:342 msgid "English" msgstr "" -#: InvenTree/settings.py:342 +#: InvenTree/settings.py:343 msgid "German" msgstr "" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:344 msgid "French" msgstr "" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:345 msgid "Polish" msgstr "" @@ -143,7 +143,8 @@ msgstr "" msgid "Returned" msgstr "" -#: InvenTree/status_codes.py:136 order/templates/order/sales_order_base.html:98 +#: InvenTree/status_codes.py:136 +#: order/templates/order/sales_order_base.html:103 msgid "Shipped" msgstr "" @@ -198,7 +199,7 @@ msgstr "" msgid "Overage must be an integer value or a percentage" msgstr "" -#: InvenTree/views.py:639 +#: InvenTree/views.py:661 msgid "Database Statistics" msgstr "" @@ -250,7 +251,7 @@ msgstr "" msgid "Serial numbers" msgstr "" -#: build/forms.py:64 stock/forms.py:107 +#: build/forms.py:64 stock/forms.py:111 msgid "Enter unique serial numbers (or leave blank)" msgstr "" @@ -262,7 +263,7 @@ msgstr "" msgid "Build quantity must be integer value for trackable parts" msgstr "" -#: build/models.py:73 build/templates/build/build_base.html:65 +#: build/models.py:73 build/templates/build/build_base.html:70 msgid "Build Title" msgstr "" @@ -270,7 +271,7 @@ msgstr "" msgid "Brief description of the build" msgstr "" -#: build/models.py:84 build/templates/build/build_base.html:86 +#: build/models.py:84 build/templates/build/build_base.html:91 msgid "Parent Build" msgstr "" @@ -280,18 +281,17 @@ msgstr "" #: build/models.py:90 build/templates/build/allocate.html:329 #: build/templates/build/auto_allocate.html:19 -#: build/templates/build/build_base.html:70 +#: build/templates/build/build_base.html:75 #: build/templates/build/detail.html:22 order/models.py:501 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:147 -#: order/templates/order/receive_parts.html:19 part/models.py:241 +#: order/templates/order/receive_parts.html:19 part/models.py:293 #: part/templates/part/part_app_base.html:7 -#: part/templates/part/set_category.html:13 -#: stock/templates/stock/item_installed.html:60 -#: templates/InvenTree/search.html:123 templates/js/barcode.html:336 -#: templates/js/bom.html:124 templates/js/build.html:47 -#: templates/js/company.html:137 templates/js/part.html:223 -#: templates/js/stock.html:421 +#: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 +#: templates/js/barcode.html:336 templates/js/bom.html:124 +#: templates/js/build.html:47 templates/js/company.html:137 +#: templates/js/part.html:184 templates/js/part.html:289 +#: templates/js/stock.html:421 templates/js/stock.html:977 msgid "Part" msgstr "" @@ -325,7 +325,7 @@ msgstr "" msgid "Number of parts to build" msgstr "" -#: build/models.py:128 part/templates/part/part_base.html:142 +#: build/models.py:128 part/templates/part/part_base.html:145 msgid "Build Status" msgstr "" @@ -333,7 +333,7 @@ msgstr "" msgid "Build status code" msgstr "" -#: build/models.py:136 stock/models.py:371 +#: build/models.py:136 stock/models.py:387 msgid "Batch Code" msgstr "" @@ -344,12 +344,12 @@ msgstr "" #: build/models.py:155 build/templates/build/detail.html:55 #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 -#: part/templates/part/detail.html:80 part/templates/part/part_base.html:89 -#: stock/models.py:365 stock/templates/stock/item_base.html:232 +#: part/templates/part/detail.html:80 part/templates/part/part_base.html:92 +#: stock/models.py:381 stock/templates/stock/item_base.html:244 msgid "External Link" msgstr "" -#: build/models.py:156 stock/models.py:367 +#: build/models.py:156 stock/models.py:383 msgid "Link to external URL" msgstr "" @@ -357,10 +357,10 @@ msgstr "" #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:15 #: order/templates/order/purchase_order_detail.html:202 #: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:67 -#: stock/forms.py:281 stock/forms.py:309 stock/models.py:433 -#: stock/models.py:1353 stock/templates/stock/tabs.html:26 -#: templates/js/barcode.html:391 templates/js/bom.html:219 -#: templates/js/stock.html:116 templates/js/stock.html:534 +#: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 +#: stock/models.py:1404 stock/templates/stock/tabs.html:26 +#: templates/js/barcode.html:391 templates/js/bom.html:223 +#: templates/js/stock.html:116 templates/js/stock.html:543 msgid "Notes" msgstr "" @@ -404,7 +404,7 @@ msgstr "" #: build/templates/build/allocate.html:17 #: company/templates/company/detail_part.html:18 order/views.py:779 -#: part/templates/part/category.html:107 +#: part/templates/part/category.html:112 msgid "Order Parts" msgstr "" @@ -420,24 +420,24 @@ msgstr "" msgid "Unallocate" msgstr "" -#: build/templates/build/allocate.html:87 templates/stock_table.html:8 +#: build/templates/build/allocate.html:87 templates/stock_table.html:9 msgid "New Stock Item" msgstr "" -#: build/templates/build/allocate.html:88 stock/views.py:1327 +#: build/templates/build/allocate.html:88 stock/views.py:1428 msgid "Create new Stock Item" msgstr "" #: build/templates/build/allocate.html:170 #: order/templates/order/sales_order_detail.html:68 -#: order/templates/order/sales_order_detail.html:150 stock/models.py:359 -#: stock/templates/stock/item_base.html:148 +#: order/templates/order/sales_order_detail.html:150 stock/models.py:375 +#: stock/templates/stock/item_base.html:156 msgid "Serial Number" msgstr "" #: build/templates/build/allocate.html:172 #: build/templates/build/auto_allocate.html:20 -#: build/templates/build/build_base.html:75 +#: build/templates/build/build_base.html:80 #: build/templates/build/detail.html:27 #: company/templates/company/supplier_part_pricing.html:71 #: order/templates/order/order_wizard/select_parts.html:32 @@ -446,22 +446,22 @@ msgstr "" #: order/templates/order/sales_order_detail.html:152 #: part/templates/part/allocation.html:16 #: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:80 +#: part/templates/part/sale_prices.html:80 stock/forms.py:297 #: stock/templates/stock/item_base.html:26 #: stock/templates/stock/item_base.html:32 -#: stock/templates/stock/item_base.html:154 +#: stock/templates/stock/item_base.html:162 #: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.html:338 #: templates/js/bom.html:162 templates/js/build.html:58 -#: templates/js/stock.html:681 +#: templates/js/stock.html:690 templates/js/stock.html:905 msgid "Quantity" msgstr "" #: build/templates/build/allocate.html:186 -#: build/templates/build/auto_allocate.html:21 stock/forms.py:279 -#: stock/templates/stock/item_base.html:186 +#: build/templates/build/auto_allocate.html:21 stock/forms.py:336 +#: stock/templates/stock/item_base.html:198 #: stock/templates/stock/stock_adjust.html:17 -#: templates/InvenTree/search.html:173 templates/js/barcode.html:337 -#: templates/js/stock.html:512 +#: templates/InvenTree/search.html:183 templates/js/barcode.html:337 +#: templates/js/stock.html:518 msgid "Location" msgstr "" @@ -475,7 +475,7 @@ msgstr "" msgid "Delete stock allocation" msgstr "" -#: build/templates/build/allocate.html:238 templates/js/bom.html:330 +#: build/templates/build/allocate.html:238 templates/js/bom.html:334 msgid "No BOM items found" msgstr "" @@ -484,12 +484,12 @@ msgstr "" #: company/templates/company/supplier_part_detail.html:27 #: order/templates/order/purchase_order_detail.html:159 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 -#: stock/templates/stock/item_installed.html:83 -#: templates/InvenTree/search.html:137 templates/js/bom.html:147 +#: templates/InvenTree/search.html:147 templates/js/bom.html:147 #: templates/js/company.html:56 templates/js/order.html:159 #: templates/js/order.html:234 templates/js/part.html:120 -#: templates/js/part.html:279 templates/js/part.html:460 -#: templates/js/stock.html:444 templates/js/stock.html:662 +#: templates/js/part.html:203 templates/js/part.html:345 +#: templates/js/part.html:526 templates/js/stock.html:444 +#: templates/js/stock.html:671 msgid "Description" msgstr "" @@ -499,8 +499,8 @@ msgstr "" msgid "Reference" msgstr "" -#: build/templates/build/allocate.html:347 part/models.py:1348 -#: templates/js/part.html:464 templates/js/table_filters.html:121 +#: build/templates/build/allocate.html:347 part/models.py:1401 +#: templates/js/part.html:530 templates/js/table_filters.html:121 msgid "Required" msgstr "" @@ -547,7 +547,7 @@ msgstr "" #: build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 -#: stock/templates/stock/item_base.html:211 templates/js/build.html:39 +#: stock/templates/stock/item_base.html:223 templates/js/build.html:39 #: templates/navbar.html:20 msgid "Build" msgstr "" @@ -560,40 +560,49 @@ msgstr "" msgid "This build is a child of Build" msgstr "" -#: build/templates/build/build_base.html:61 build/templates/build/detail.html:9 +#: build/templates/build/build_base.html:39 +#: company/templates/company/company_base.html:27 +#: order/templates/order/order_base.html:28 +#: order/templates/order/sales_order_base.html:38 +#: part/templates/part/category.html:13 part/templates/part/part_base.html:32 +#: stock/templates/stock/item_base.html:69 +#: stock/templates/stock/location.html:12 +msgid "Admin view" +msgstr "" + +#: build/templates/build/build_base.html:66 build/templates/build/detail.html:9 msgid "Build Details" msgstr "" -#: build/templates/build/build_base.html:80 +#: build/templates/build/build_base.html:85 #: build/templates/build/detail.html:42 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:264 -#: stock/templates/stock/item_installed.html:111 -#: templates/InvenTree/search.html:165 templates/js/barcode.html:42 -#: templates/js/build.html:63 templates/js/order.html:164 -#: templates/js/order.html:239 templates/js/stock.html:499 +#: stock/templates/stock/item_base.html:276 templates/InvenTree/search.html:175 +#: templates/js/barcode.html:42 templates/js/build.html:63 +#: templates/js/order.html:164 templates/js/order.html:239 +#: templates/js/stock.html:505 templates/js/stock.html:913 msgid "Status" msgstr "" -#: build/templates/build/build_base.html:93 order/models.py:499 +#: build/templates/build/build_base.html:98 order/models.py:499 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:174 templates/js/order.html:213 +#: stock/templates/stock/item_base.html:186 templates/js/order.html:213 msgid "Sales Order" msgstr "" -#: build/templates/build/build_base.html:99 +#: build/templates/build/build_base.html:104 msgid "BOM Price" msgstr "" -#: build/templates/build/build_base.html:104 +#: build/templates/build/build_base.html:109 msgid "BOM pricing is incomplete" msgstr "" -#: build/templates/build/build_base.html:107 +#: build/templates/build/build_base.html:112 msgid "No pricing information" msgstr "" @@ -648,15 +657,15 @@ msgid "Stock can be taken from any available location." msgstr "" #: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:204 -#: stock/templates/stock/item_installed.html:119 templates/js/stock.html:507 -#: templates/js/table_filters.html:34 templates/js/table_filters.html:100 +#: stock/templates/stock/item_base.html:216 templates/js/stock.html:513 +#: templates/js/stock.html:920 templates/js/table_filters.html:34 +#: templates/js/table_filters.html:100 msgid "Batch" msgstr "" #: build/templates/build/detail.html:61 -#: order/templates/order/order_base.html:93 -#: order/templates/order/sales_order_base.html:92 templates/js/build.html:71 +#: order/templates/order/order_base.html:98 +#: order/templates/order/sales_order_base.html:97 templates/js/build.html:71 msgid "Created" msgstr "" @@ -769,7 +778,7 @@ msgstr "" msgid "Invalid location selected" msgstr "" -#: build/views.py:296 stock/views.py:1520 +#: build/views.py:296 stock/views.py:1621 #, python-brace-format msgid "The following serial numbers already exist: ({sn})" msgstr "" @@ -878,7 +887,7 @@ msgstr "" msgid "Description of the company" msgstr "" -#: company/models.py:91 company/templates/company/company_base.html:48 +#: company/models.py:91 company/templates/company/company_base.html:53 #: templates/js/company.html:61 msgid "Website" msgstr "" @@ -887,7 +896,7 @@ msgstr "" msgid "Company website URL" msgstr "" -#: company/models.py:94 company/templates/company/company_base.html:55 +#: company/models.py:94 company/templates/company/company_base.html:60 msgid "Address" msgstr "" @@ -903,7 +912,7 @@ msgstr "" msgid "Contact phone number" msgstr "" -#: company/models.py:101 company/templates/company/company_base.html:69 +#: company/models.py:101 company/templates/company/company_base.html:74 msgid "Email" msgstr "" @@ -911,7 +920,7 @@ msgstr "" msgid "Contact email address" msgstr "" -#: company/models.py:104 company/templates/company/company_base.html:76 +#: company/models.py:104 company/templates/company/company_base.html:81 msgid "Contact" msgstr "" @@ -935,8 +944,8 @@ msgstr "" msgid "Does this company manufacture parts?" msgstr "" -#: company/models.py:279 stock/models.py:319 -#: stock/templates/stock/item_base.html:140 +#: company/models.py:279 stock/models.py:335 +#: stock/templates/stock/item_base.html:148 msgid "Base Part" msgstr "" @@ -986,12 +995,12 @@ msgstr "" msgid "Company" msgstr "" -#: company/templates/company/company_base.html:42 +#: company/templates/company/company_base.html:47 #: company/templates/company/detail.html:8 msgid "Company Details" msgstr "" -#: company/templates/company/company_base.html:62 +#: company/templates/company/company_base.html:67 msgid "Phone" msgstr "" @@ -1005,16 +1014,16 @@ msgstr "" #: company/templates/company/detail.html:21 #: company/templates/company/supplier_part_base.html:66 #: company/templates/company/supplier_part_detail.html:21 -#: order/templates/order/order_base.html:74 +#: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:239 templates/js/company.html:48 +#: stock/templates/stock/item_base.html:251 templates/js/company.html:48 #: templates/js/company.html:162 templates/js/order.html:146 msgid "Supplier" msgstr "" #: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:73 stock/models.py:354 -#: stock/models.py:355 stock/templates/stock/item_base.html:161 +#: order/templates/order/sales_order_base.html:78 stock/models.py:370 +#: stock/models.py:371 stock/templates/stock/item_base.html:169 #: templates/js/company.html:40 templates/js/order.html:221 msgid "Customer" msgstr "" @@ -1030,18 +1039,18 @@ msgstr "" #: company/templates/company/detail_part.html:13 #: order/templates/order/purchase_order_detail.html:67 -#: part/templates/part/supplier.html:13 templates/js/stock.html:788 +#: part/templates/part/supplier.html:13 templates/js/stock.html:797 msgid "New Supplier Part" msgstr "" #: company/templates/company/detail_part.html:15 -#: part/templates/part/category.html:104 part/templates/part/supplier.html:15 -#: stock/templates/stock/item_installed.html:16 templates/stock_table.html:10 +#: part/templates/part/category.html:109 part/templates/part/supplier.html:15 +#: templates/stock_table.html:11 msgid "Options" msgstr "" #: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:107 +#: part/templates/part/category.html:112 msgid "Order parts" msgstr "" @@ -1054,7 +1063,7 @@ msgid "Delete Parts" msgstr "" #: company/templates/company/detail_part.html:43 -#: part/templates/part/category.html:102 templates/js/stock.html:782 +#: part/templates/part/category.html:107 templates/js/stock.html:791 msgid "New Part" msgstr "" @@ -1086,8 +1095,8 @@ msgstr "" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:101 part/templates/part/category.html:108 -#: part/templates/part/stock.html:51 templates/stock_table.html:5 +#: part/templates/part/category.html:106 part/templates/part/category.html:113 +#: part/templates/part/stock.html:51 templates/stock_table.html:6 msgid "Export" msgstr "" @@ -1143,8 +1152,8 @@ msgid "New Sales Order" msgstr "" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:328 -#: stock/templates/stock/item_base.html:244 templates/js/company.html:178 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:344 +#: stock/templates/stock/item_base.html:256 templates/js/company.html:178 msgid "Supplier Part" msgstr "" @@ -1196,7 +1205,7 @@ msgid "Pricing Information" msgstr "" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2108 +#: part/templates/part/sale_prices.html:13 part/views.py:2149 msgid "Add Price Break" msgstr "" @@ -1206,7 +1215,7 @@ msgid "No price break information found" msgstr "" #: company/templates/company/supplier_part_pricing.html:76 -#: part/templates/part/sale_prices.html:85 templates/js/bom.html:203 +#: part/templates/part/sale_prices.html:85 templates/js/bom.html:207 msgid "Price" msgstr "" @@ -1230,9 +1239,8 @@ msgstr "" #: company/templates/company/supplier_part_tabs.html:8 #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 -#: stock/templates/stock/item_installed.html:91 -#: stock/templates/stock/location.html:12 templates/InvenTree/search.html:145 -#: templates/js/part.html:124 templates/js/part.html:306 +#: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 +#: templates/js/part.html:124 templates/js/part.html:372 #: templates/js/stock.html:452 templates/navbar.html:19 msgid "Stock" msgstr "" @@ -1242,9 +1250,10 @@ msgid "Orders" msgstr "" #: company/templates/company/tabs.html:9 -#: order/templates/order/receive_parts.html:14 part/models.py:242 -#: part/templates/part/cat_link.html:7 part/templates/part/category.html:83 -#: templates/navbar.html:18 templates/stats.html:8 templates/stats.html:17 +#: order/templates/order/receive_parts.html:14 part/models.py:294 +#: part/templates/part/cat_link.html:7 part/templates/part/category.html:88 +#: part/templates/part/category_tabs.html:6 templates/navbar.html:18 +#: templates/stats.html:8 templates/stats.html:17 msgid "Parts" msgstr "" @@ -1313,7 +1322,7 @@ msgstr "" msgid "Edit Supplier Part" msgstr "" -#: company/views.py:269 templates/js/stock.html:789 +#: company/views.py:269 templates/js/stock.html:798 msgid "Create new Supplier Part" msgstr "" @@ -1321,15 +1330,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:404 part/views.py:2112 +#: company/views.py:404 part/views.py:2153 msgid "Added new price break" msgstr "" -#: company/views.py:441 part/views.py:2157 +#: company/views.py:441 part/views.py:2198 msgid "Edit Price Break" msgstr "" -#: company/views.py:456 part/views.py:2171 +#: company/views.py:456 part/views.py:2212 msgid "Delete Price Break" msgstr "" @@ -1366,11 +1375,11 @@ msgid "Mark order as complete" msgstr "" #: order/forms.py:46 order/forms.py:57 -#: order/templates/order/sales_order_base.html:49 +#: order/templates/order/sales_order_base.html:54 msgid "Cancel order" msgstr "" -#: order/forms.py:68 order/templates/order/sales_order_base.html:46 +#: order/forms.py:68 order/templates/order/sales_order_base.html:51 msgid "Ship order" msgstr "" @@ -1423,7 +1432,7 @@ msgid "Date order was completed" msgstr "" #: order/models.py:185 order/models.py:259 part/views.py:1304 -#: stock/models.py:239 stock/models.py:754 +#: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "" @@ -1461,7 +1470,7 @@ msgstr "" #: order/models.py:466 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:23 -#: stock/templates/stock/item_base.html:218 templates/js/order.html:138 +#: stock/templates/stock/item_base.html:230 templates/js/order.html:138 msgid "Purchase Order" msgstr "" @@ -1503,32 +1512,32 @@ msgstr "" msgid "Are you sure you want to delete this attachment?" msgstr "" -#: order/templates/order/order_base.html:59 -msgid "Purchase Order Details" -msgstr "" - #: order/templates/order/order_base.html:64 -#: order/templates/order/sales_order_base.html:63 -msgid "Order Reference" +msgid "Purchase Order Details" msgstr "" #: order/templates/order/order_base.html:69 #: order/templates/order/sales_order_base.html:68 +msgid "Order Reference" +msgstr "" + +#: order/templates/order/order_base.html:74 +#: order/templates/order/sales_order_base.html:73 msgid "Order Status" msgstr "" -#: order/templates/order/order_base.html:80 templates/js/order.html:153 +#: order/templates/order/order_base.html:85 templates/js/order.html:153 msgid "Supplier Reference" msgstr "" -#: order/templates/order/order_base.html:99 +#: order/templates/order/order_base.html:104 msgid "Issued" msgstr "" -#: order/templates/order/order_base.html:106 +#: order/templates/order/order_base.html:111 #: order/templates/order/purchase_order_detail.html:182 #: order/templates/order/receive_parts.html:22 -#: order/templates/order/sales_order_base.html:105 +#: order/templates/order/sales_order_base.html:110 msgid "Received" msgstr "" @@ -1607,14 +1616,14 @@ msgstr "" #: order/templates/order/purchase_order_detail.html:38 #: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:153 part/templates/part/category.html:194 -#: templates/js/stock.html:794 +#: part/templates/part/category.html:161 part/templates/part/category.html:202 +#: templates/js/stock.html:803 msgid "New Location" msgstr "" #: order/templates/order/purchase_order_detail.html:39 #: order/templates/order/purchase_order_detail.html:119 -#: stock/templates/stock/location.html:16 +#: stock/templates/stock/location.html:21 msgid "Create new stock location" msgstr "" @@ -1649,7 +1658,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:132 templates/js/part.html:322 +#: part/templates/part/part_base.html:135 templates/js/part.html:388 msgid "On Order" msgstr "" @@ -1665,15 +1674,15 @@ msgstr "" msgid "This SalesOrder has not been fully allocated" msgstr "" -#: order/templates/order/sales_order_base.html:42 +#: order/templates/order/sales_order_base.html:47 msgid "Packing List" msgstr "" -#: order/templates/order/sales_order_base.html:58 +#: order/templates/order/sales_order_base.html:63 msgid "Sales Order Details" msgstr "" -#: order/templates/order/sales_order_base.html:79 templates/js/order.html:228 +#: order/templates/order/sales_order_base.html:84 templates/js/order.html:228 msgid "Customer Reference" msgstr "" @@ -1881,12 +1890,12 @@ msgstr "" msgid "Remove allocation" msgstr "" -#: part/bom.py:138 part/templates/part/category.html:50 +#: part/bom.py:138 part/templates/part/category.html:55 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "" -#: part/bom.py:139 part/templates/part/part_base.html:105 +#: part/bom.py:139 part/templates/part/part_base.html:108 msgid "Available Stock" msgstr "" @@ -1903,11 +1912,11 @@ msgstr "" msgid "Error reading BOM file (incorrect row size)" msgstr "" -#: part/forms.py:57 stock/forms.py:250 +#: part/forms.py:57 stock/forms.py:254 msgid "File Format" msgstr "" -#: part/forms.py:57 stock/forms.py:250 +#: part/forms.py:57 stock/forms.py:254 msgid "Select output file format" msgstr "" @@ -1983,11 +1992,11 @@ msgstr "" msgid "Confirm part creation" msgstr "" -#: part/forms.py:247 +#: part/forms.py:248 msgid "Input quantity for price calculation" msgstr "" -#: part/forms.py:250 +#: part/forms.py:251 msgid "Select currency for price calculation" msgstr "" @@ -2003,222 +2012,226 @@ msgstr "" msgid "Part Category" msgstr "" -#: part/models.py:76 part/templates/part/category.html:13 -#: part/templates/part/category.html:78 templates/stats.html:12 +#: part/models.py:76 part/templates/part/category.html:18 +#: part/templates/part/category.html:83 templates/stats.html:12 msgid "Part Categories" msgstr "" -#: part/models.py:293 part/models.py:303 +#: part/models.py:345 part/models.py:355 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "" -#: part/models.py:383 +#: part/models.py:435 msgid "Next available serial numbers are" msgstr "" -#: part/models.py:387 +#: part/models.py:439 msgid "Next available serial number is" msgstr "" -#: part/models.py:392 +#: part/models.py:444 msgid "Most recent serial number is" msgstr "" -#: part/models.py:470 +#: part/models.py:522 msgid "Part must be unique for name, IPN and revision" msgstr "" -#: part/models.py:485 part/templates/part/detail.html:19 +#: part/models.py:537 part/templates/part/detail.html:19 msgid "Part name" msgstr "" -#: part/models.py:489 +#: part/models.py:541 msgid "Is this part a template part?" msgstr "" -#: part/models.py:498 +#: part/models.py:550 msgid "Is this part a variant of another part?" msgstr "" -#: part/models.py:500 +#: part/models.py:552 msgid "Part description" msgstr "" -#: part/models.py:502 +#: part/models.py:554 msgid "Part keywords to improve visibility in search results" msgstr "" -#: part/models.py:507 +#: part/models.py:559 msgid "Part category" msgstr "" -#: part/models.py:509 +#: part/models.py:561 msgid "Internal Part Number" msgstr "" -#: part/models.py:511 +#: part/models.py:563 msgid "Part revision or version number" msgstr "" -#: part/models.py:513 +#: part/models.py:565 msgid "Link to extenal URL" msgstr "" -#: part/models.py:525 +#: part/models.py:577 msgid "Where is this item normally stored?" msgstr "" -#: part/models.py:569 +#: part/models.py:621 msgid "Default supplier part" msgstr "" -#: part/models.py:572 +#: part/models.py:624 msgid "Minimum allowed stock level" msgstr "" -#: part/models.py:574 +#: part/models.py:626 msgid "Stock keeping units for this part" msgstr "" -#: part/models.py:576 +#: part/models.py:628 msgid "Can this part be built from other parts?" msgstr "" -#: part/models.py:578 +#: part/models.py:630 msgid "Can this part be used to build other parts?" msgstr "" -#: part/models.py:580 +#: part/models.py:632 msgid "Does this part have tracking for unique items?" msgstr "" -#: part/models.py:582 +#: part/models.py:634 msgid "Can this part be purchased from external suppliers?" msgstr "" -#: part/models.py:584 +#: part/models.py:636 msgid "Can this part be sold to customers?" msgstr "" -#: part/models.py:586 +#: part/models.py:638 msgid "Is this part active?" msgstr "" -#: part/models.py:588 +#: part/models.py:640 msgid "Is this a virtual part, such as a software product or license?" msgstr "" -#: part/models.py:590 +#: part/models.py:642 msgid "Part notes - supports Markdown formatting" msgstr "" -#: part/models.py:592 +#: part/models.py:644 msgid "Stored BOM checksum" msgstr "" -#: part/models.py:1300 +#: part/models.py:1353 msgid "Test templates can only be created for trackable parts" msgstr "" -#: part/models.py:1317 +#: part/models.py:1370 msgid "Test with this name already exists for this part" msgstr "" -#: part/models.py:1336 templates/js/part.html:455 templates/js/stock.html:92 +#: part/models.py:1389 templates/js/part.html:521 templates/js/stock.html:92 msgid "Test Name" msgstr "" -#: part/models.py:1337 +#: part/models.py:1390 msgid "Enter a name for the test" msgstr "" -#: part/models.py:1342 +#: part/models.py:1395 msgid "Test Description" msgstr "" -#: part/models.py:1343 +#: part/models.py:1396 msgid "Enter description for this test" msgstr "" -#: part/models.py:1349 +#: part/models.py:1402 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1354 templates/js/part.html:472 +#: part/models.py:1407 templates/js/part.html:538 msgid "Requires Value" msgstr "" -#: part/models.py:1355 +#: part/models.py:1408 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1360 templates/js/part.html:479 +#: part/models.py:1413 templates/js/part.html:545 msgid "Requires Attachment" msgstr "" -#: part/models.py:1361 +#: part/models.py:1414 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1394 +#: part/models.py:1447 msgid "Parameter template name must be unique" msgstr "" -#: part/models.py:1399 +#: part/models.py:1452 msgid "Parameter Name" msgstr "" -#: part/models.py:1401 +#: part/models.py:1454 msgid "Parameter Units" msgstr "" -#: part/models.py:1427 +#: part/models.py:1480 msgid "Parent Part" msgstr "" -#: part/models.py:1429 +#: part/models.py:1482 msgid "Parameter Template" msgstr "" -#: part/models.py:1431 +#: part/models.py:1484 msgid "Parameter Value" msgstr "" -#: part/models.py:1467 +#: part/models.py:1521 msgid "Select parent part" msgstr "" -#: part/models.py:1475 +#: part/models.py:1529 msgid "Select part to be used in BOM" msgstr "" -#: part/models.py:1481 +#: part/models.py:1535 msgid "BOM quantity for this BOM item" msgstr "" -#: part/models.py:1484 +#: part/models.py:1537 +msgid "This BOM item is optional" +msgstr "" + +#: part/models.py:1540 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "" -#: part/models.py:1487 +#: part/models.py:1543 msgid "BOM item reference" msgstr "" -#: part/models.py:1490 +#: part/models.py:1546 msgid "BOM item notes" msgstr "" -#: part/models.py:1492 +#: part/models.py:1548 msgid "BOM line checksum" msgstr "" -#: part/models.py:1556 part/views.py:1310 part/views.py:1362 -#: stock/models.py:229 +#: part/models.py:1612 part/views.py:1310 part/views.py:1362 +#: stock/models.py:231 msgid "Quantity must be integer value for trackable parts" msgstr "" -#: part/models.py:1565 +#: part/models.py:1621 msgid "BOM Item" msgstr "" @@ -2237,14 +2250,14 @@ msgstr "" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:58 -#: stock/templates/stock/item_base.html:226 +#: stock/templates/stock/item_base.html:238 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.html:112 -#: templates/js/stock.html:651 +#: templates/js/stock.html:660 templates/js/stock.html:896 msgid "Stock Item" msgstr "" #: part/templates/part/allocation.html:20 -#: stock/templates/stock/item_base.html:180 +#: stock/templates/stock/item_base.html:192 msgid "Build Order" msgstr "" @@ -2360,91 +2373,95 @@ msgstr "" msgid "Each part must already exist in the database" msgstr "" -#: part/templates/part/category.html:14 +#: part/templates/part/category.html:19 msgid "All parts" msgstr "" -#: part/templates/part/category.html:18 part/views.py:1935 +#: part/templates/part/category.html:23 part/views.py:1976 msgid "Create new part category" msgstr "" -#: part/templates/part/category.html:22 +#: part/templates/part/category.html:27 msgid "Edit part category" msgstr "" -#: part/templates/part/category.html:25 +#: part/templates/part/category.html:30 msgid "Delete part category" msgstr "" -#: part/templates/part/category.html:34 part/templates/part/category.html:73 +#: part/templates/part/category.html:39 part/templates/part/category.html:78 msgid "Category Details" msgstr "" -#: part/templates/part/category.html:39 +#: part/templates/part/category.html:44 msgid "Category Path" msgstr "" -#: part/templates/part/category.html:44 +#: part/templates/part/category.html:49 msgid "Category Description" msgstr "" -#: part/templates/part/category.html:57 part/templates/part/detail.html:64 +#: part/templates/part/category.html:62 part/templates/part/detail.html:64 msgid "Keywords" msgstr "" -#: part/templates/part/category.html:63 +#: part/templates/part/category.html:68 msgid "Subcategories" msgstr "" -#: part/templates/part/category.html:68 +#: part/templates/part/category.html:73 msgid "Parts (Including subcategories)" msgstr "" -#: part/templates/part/category.html:101 +#: part/templates/part/category.html:106 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:102 part/views.py:491 +#: part/templates/part/category.html:107 part/views.py:491 msgid "Create new part" msgstr "" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:111 msgid "Set category" msgstr "" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:111 msgid "Set Category" msgstr "" -#: part/templates/part/category.html:108 +#: part/templates/part/category.html:113 msgid "Export Data" msgstr "" -#: part/templates/part/category.html:154 +#: part/templates/part/category.html:162 msgid "Create new location" msgstr "" -#: part/templates/part/category.html:159 part/templates/part/category.html:188 +#: part/templates/part/category.html:167 part/templates/part/category.html:196 msgid "New Category" msgstr "" -#: part/templates/part/category.html:160 +#: part/templates/part/category.html:168 msgid "Create new category" msgstr "" -#: part/templates/part/category.html:189 +#: part/templates/part/category.html:197 msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:195 stock/views.py:1213 +#: part/templates/part/category.html:203 stock/views.py:1314 msgid "Create new Stock Location" msgstr "" +#: part/templates/part/category_tabs.html:9 +msgid "Parametric Table" +msgstr "" + #: part/templates/part/detail.html:9 msgid "Part Details" msgstr "" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:82 +#: part/templates/part/detail.html:25 part/templates/part/part_base.html:85 #: templates/js/part.html:112 msgid "IPN" msgstr "" @@ -2466,7 +2483,7 @@ msgid "Variant Of" msgstr "" #: part/templates/part/detail.html:70 part/templates/part/set_category.html:15 -#: templates/js/part.html:293 +#: templates/js/part.html:359 msgid "Category" msgstr "" @@ -2506,8 +2523,8 @@ msgstr "" msgid "Part is not a virtual part" msgstr "" -#: part/templates/part/detail.html:145 stock/forms.py:244 -#: templates/js/table_filters.html:183 +#: part/templates/part/detail.html:145 stock/forms.py:248 +#: templates/js/table_filters.html:188 msgid "Template" msgstr "" @@ -2519,7 +2536,7 @@ msgstr "" msgid "Part is not a template part" msgstr "" -#: part/templates/part/detail.html:154 templates/js/table_filters.html:195 +#: part/templates/part/detail.html:154 templates/js/table_filters.html:200 msgid "Assembly" msgstr "" @@ -2531,7 +2548,7 @@ msgstr "" msgid "Part cannot be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:163 templates/js/table_filters.html:199 +#: part/templates/part/detail.html:163 templates/js/table_filters.html:204 msgid "Component" msgstr "" @@ -2543,7 +2560,7 @@ msgstr "" msgid "Part cannot be used in assemblies" msgstr "" -#: part/templates/part/detail.html:172 templates/js/table_filters.html:211 +#: part/templates/part/detail.html:172 templates/js/table_filters.html:216 msgid "Trackable" msgstr "" @@ -2563,7 +2580,7 @@ msgstr "" msgid "Part can be purchased from external suppliers" msgstr "" -#: part/templates/part/detail.html:190 templates/js/table_filters.html:207 +#: part/templates/part/detail.html:190 templates/js/table_filters.html:212 msgid "Salable" msgstr "" @@ -2575,7 +2592,7 @@ msgstr "" msgid "Part cannot be sold to customers" msgstr "" -#: part/templates/part/detail.html:199 templates/js/table_filters.html:178 +#: part/templates/part/detail.html:199 templates/js/table_filters.html:183 msgid "Active" msgstr "" @@ -2607,7 +2624,7 @@ msgstr "" msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:21 stock/models.py:1340 +#: part/templates/part/params.html:21 stock/models.py:1391 #: templates/js/stock.html:112 msgid "Value" msgstr "" @@ -2636,70 +2653,70 @@ msgstr "" msgid "This part is a variant of" msgstr "" -#: part/templates/part/part_base.html:33 templates/js/company.html:153 -#: templates/js/part.html:270 +#: part/templates/part/part_base.html:36 templates/js/company.html:153 +#: templates/js/part.html:336 msgid "Inactive" msgstr "" -#: part/templates/part/part_base.html:40 +#: part/templates/part/part_base.html:43 msgid "Star this part" msgstr "" -#: part/templates/part/part_base.html:46 -#: stock/templates/stock/item_base.html:78 -#: stock/templates/stock/location.html:22 -msgid "Barcode actions" -msgstr "" - -#: part/templates/part/part_base.html:48 -#: stock/templates/stock/item_base.html:80 -#: stock/templates/stock/location.html:24 -msgid "Show QR Code" -msgstr "" - #: part/templates/part/part_base.html:49 #: stock/templates/stock/item_base.html:81 -#: stock/templates/stock/location.html:25 +#: stock/templates/stock/location.html:27 +msgid "Barcode actions" +msgstr "" + +#: part/templates/part/part_base.html:51 +#: stock/templates/stock/item_base.html:83 +#: stock/templates/stock/location.html:29 +msgid "Show QR Code" +msgstr "" + +#: part/templates/part/part_base.html:52 +#: stock/templates/stock/item_base.html:84 +#: stock/templates/stock/location.html:30 msgid "Print Label" msgstr "" -#: part/templates/part/part_base.html:53 +#: part/templates/part/part_base.html:56 msgid "Show pricing information" msgstr "" -#: part/templates/part/part_base.html:67 +#: part/templates/part/part_base.html:70 msgid "Part actions" msgstr "" -#: part/templates/part/part_base.html:69 +#: part/templates/part/part_base.html:72 msgid "Duplicate part" msgstr "" -#: part/templates/part/part_base.html:70 +#: part/templates/part/part_base.html:73 msgid "Edit part" msgstr "" -#: part/templates/part/part_base.html:72 +#: part/templates/part/part_base.html:75 msgid "Delete part" msgstr "" -#: part/templates/part/part_base.html:111 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:114 templates/js/table_filters.html:65 msgid "In Stock" msgstr "" -#: part/templates/part/part_base.html:118 +#: part/templates/part/part_base.html:121 msgid "Allocated to Build Orders" msgstr "" -#: part/templates/part/part_base.html:125 +#: part/templates/part/part_base.html:128 msgid "Allocated to Sales Orders" msgstr "" -#: part/templates/part/part_base.html:147 +#: part/templates/part/part_base.html:150 msgid "Can Build" msgstr "" -#: part/templates/part/part_base.html:153 +#: part/templates/part/part_base.html:156 msgid "Underway" msgstr "" @@ -2743,8 +2760,8 @@ msgstr "" msgid "Part Stock" msgstr "" -#: part/templates/part/stock_count.html:7 templates/js/bom.html:193 -#: templates/js/part.html:330 +#: part/templates/part/stock_count.html:7 templates/js/bom.html:197 +#: templates/js/part.html:396 msgid "No Stock" msgstr "" @@ -2784,7 +2801,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:270 +#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:282 msgid "Tests" msgstr "" @@ -2961,27 +2978,27 @@ msgstr "" msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1886 +#: part/views.py:1927 msgid "Edit Part Category" msgstr "" -#: part/views.py:1921 +#: part/views.py:1962 msgid "Delete Part Category" msgstr "" -#: part/views.py:1927 +#: part/views.py:1968 msgid "Part category was deleted" msgstr "" -#: part/views.py:1986 +#: part/views.py:2027 msgid "Create BOM item" msgstr "" -#: part/views.py:2052 +#: part/views.py:2093 msgid "Edit BOM item" msgstr "" -#: part/views.py:2100 +#: part/views.py:2141 msgid "Confim BOM item deletion" msgstr "" @@ -3013,267 +3030,295 @@ msgstr "" msgid "Asset file description" msgstr "" -#: stock/forms.py:187 +#: stock/forms.py:191 msgid "Label" msgstr "" -#: stock/forms.py:188 stock/forms.py:244 +#: stock/forms.py:192 stock/forms.py:248 msgid "Select test report template" msgstr "" -#: stock/forms.py:252 +#: stock/forms.py:256 msgid "Include stock items in sub locations" msgstr "" -#: stock/forms.py:279 +#: stock/forms.py:291 +msgid "Stock item to install" +msgstr "" + +#: stock/forms.py:298 +msgid "Stock quantity to assign" +msgstr "" + +#: stock/forms.py:326 +msgid "Must not exceed available quantity" +msgstr "" + +#: stock/forms.py:336 msgid "Destination location for uninstalled items" msgstr "" -#: stock/forms.py:281 +#: stock/forms.py:338 msgid "Add transaction note (optional)" msgstr "" -#: stock/forms.py:283 +#: stock/forms.py:340 msgid "Confirm uninstall" msgstr "" -#: stock/forms.py:283 +#: stock/forms.py:340 msgid "Confirm removal of installed stock items" msgstr "" -#: stock/forms.py:307 +#: stock/forms.py:364 msgid "Destination" msgstr "" -#: stock/forms.py:307 +#: stock/forms.py:364 msgid "Destination stock location" msgstr "" -#: stock/forms.py:309 +#: stock/forms.py:366 msgid "Add note (required)" msgstr "" -#: stock/forms.py:313 stock/views.py:795 stock/views.py:992 +#: stock/forms.py:370 stock/views.py:895 stock/views.py:1092 msgid "Confirm stock adjustment" msgstr "" -#: stock/forms.py:313 +#: stock/forms.py:370 msgid "Confirm movement of stock items" msgstr "" -#: stock/forms.py:315 +#: stock/forms.py:372 msgid "Set Default Location" msgstr "" -#: stock/forms.py:315 +#: stock/forms.py:372 msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:210 +#: stock/models.py:212 msgid "StockItem with this serial number already exists" msgstr "" -#: stock/models.py:246 +#: stock/models.py:248 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:256 stock/models.py:265 +#: stock/models.py:258 stock/models.py:267 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:257 +#: stock/models.py:259 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:278 +#: stock/models.py:281 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:311 +#: stock/models.py:287 +msgid "Item must have a build reference if is_building=True" +msgstr "" + +#: stock/models.py:294 +msgid "Build reference does not point to the same part object" +msgstr "" + +#: stock/models.py:327 msgid "Parent Stock Item" msgstr "" -#: stock/models.py:320 +#: stock/models.py:336 msgid "Base part" msgstr "" -#: stock/models.py:329 +#: stock/models.py:345 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:334 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:350 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "" -#: stock/models.py:337 +#: stock/models.py:353 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:342 +#: stock/models.py:358 stock/templates/stock/item_base.html:177 msgid "Installed In" msgstr "" -#: stock/models.py:345 +#: stock/models.py:361 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:361 +#: stock/models.py:377 msgid "Serial number for this item" msgstr "" -#: stock/models.py:373 +#: stock/models.py:389 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:377 +#: stock/models.py:393 msgid "Stock Quantity" msgstr "" -#: stock/models.py:386 +#: stock/models.py:402 msgid "Source Build" msgstr "" -#: stock/models.py:388 +#: stock/models.py:404 msgid "Build for this stock item" msgstr "" -#: stock/models.py:395 +#: stock/models.py:415 msgid "Source Purchase Order" msgstr "" -#: stock/models.py:398 +#: stock/models.py:418 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:404 +#: stock/models.py:424 msgid "Destination Sales Order" msgstr "" -#: stock/models.py:411 +#: stock/models.py:431 msgid "Destination Build Order" msgstr "" -#: stock/models.py:424 +#: stock/models.py:444 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:434 stock/templates/stock/item_notes.html:14 +#: stock/models.py:454 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:485 +#: stock/models.py:505 msgid "Assigned to Customer" msgstr "" -#: stock/models.py:487 +#: stock/models.py:507 msgid "Manually assigned to customer" msgstr "" -#: stock/models.py:500 +#: stock/models.py:520 msgid "Returned from customer" msgstr "" -#: stock/models.py:502 +#: stock/models.py:522 msgid "Returned to location" msgstr "" -#: stock/models.py:626 -msgid "Installed in stock item" +#: stock/models.py:650 +msgid "Installed into stock item" msgstr "" -#: stock/models.py:655 +#: stock/models.py:658 +msgid "Installed stock item" +msgstr "" + +#: stock/models.py:682 +msgid "Uninstalled stock item" +msgstr "" + +#: stock/models.py:701 msgid "Uninstalled into location" msgstr "" -#: stock/models.py:745 +#: stock/models.py:796 msgid "Part is not set as trackable" msgstr "" -#: stock/models.py:751 +#: stock/models.py:802 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:757 +#: stock/models.py:808 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:760 +#: stock/models.py:811 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:763 +#: stock/models.py:814 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:773 +#: stock/models.py:824 msgid "Serial numbers already exist: " msgstr "" -#: stock/models.py:798 +#: stock/models.py:849 msgid "Add serial number" msgstr "" -#: stock/models.py:801 +#: stock/models.py:852 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:912 +#: stock/models.py:963 msgid "StockItem cannot be moved as it is not in stock" msgstr "" -#: stock/models.py:1241 +#: stock/models.py:1292 msgid "Tracking entry title" msgstr "" -#: stock/models.py:1243 +#: stock/models.py:1294 msgid "Entry notes" msgstr "" -#: stock/models.py:1245 +#: stock/models.py:1296 msgid "Link to external page for further information" msgstr "" -#: stock/models.py:1305 +#: stock/models.py:1356 msgid "Value must be provided for this test" msgstr "" -#: stock/models.py:1311 +#: stock/models.py:1362 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1328 +#: stock/models.py:1379 msgid "Test" msgstr "" -#: stock/models.py:1329 +#: stock/models.py:1380 msgid "Test name" msgstr "" -#: stock/models.py:1334 +#: stock/models.py:1385 msgid "Result" msgstr "" -#: stock/models.py:1335 templates/js/table_filters.html:111 +#: stock/models.py:1386 templates/js/table_filters.html:111 msgid "Test result" msgstr "" -#: stock/models.py:1341 +#: stock/models.py:1392 msgid "Test output value" msgstr "" -#: stock/models.py:1347 +#: stock/models.py:1398 msgid "Attachment" msgstr "" -#: stock/models.py:1348 +#: stock/models.py:1399 msgid "Test result attachment" msgstr "" -#: stock/models.py:1354 +#: stock/models.py:1405 msgid "Test notes" msgstr "" @@ -3312,102 +3357,106 @@ msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" -#: stock/templates/stock/item_base.html:83 templates/js/barcode.html:283 +#: stock/templates/stock/item_base.html:86 templates/js/barcode.html:283 #: templates/js/barcode.html:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:85 +#: stock/templates/stock/item_base.html:88 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:91 +#: stock/templates/stock/item_base.html:94 msgid "Stock adjustment actions" msgstr "" -#: stock/templates/stock/item_base.html:95 -#: stock/templates/stock/location.html:33 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:98 +#: stock/templates/stock/location.html:38 templates/stock_table.html:15 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:96 templates/stock_table.html:12 +#: stock/templates/stock/item_base.html:99 templates/stock_table.html:13 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:97 templates/stock_table.html:13 +#: stock/templates/stock/item_base.html:100 templates/stock_table.html:14 msgid "Remove stock" msgstr "" -#: stock/templates/stock/item_base.html:99 +#: stock/templates/stock/item_base.html:102 msgid "Transfer stock" msgstr "" -#: stock/templates/stock/item_base.html:101 +#: stock/templates/stock/item_base.html:104 msgid "Serialize stock" msgstr "" -#: stock/templates/stock/item_base.html:105 +#: stock/templates/stock/item_base.html:108 msgid "Assign to customer" msgstr "" -#: stock/templates/stock/item_base.html:108 +#: stock/templates/stock/item_base.html:111 msgid "Return to stock" msgstr "" -#: stock/templates/stock/item_base.html:114 -#: stock/templates/stock/location.html:30 +#: stock/templates/stock/item_base.html:115 templates/js/stock.html:933 +msgid "Uninstall stock item" +msgstr "" + +#: stock/templates/stock/item_base.html:115 +msgid "Uninstall" +msgstr "" + +#: stock/templates/stock/item_base.html:122 +#: stock/templates/stock/location.html:35 msgid "Stock actions" msgstr "" -#: stock/templates/stock/item_base.html:118 +#: stock/templates/stock/item_base.html:126 msgid "Convert to variant" msgstr "" -#: stock/templates/stock/item_base.html:120 +#: stock/templates/stock/item_base.html:128 msgid "Duplicate stock item" msgstr "" -#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/item_base.html:129 msgid "Edit stock item" msgstr "" -#: stock/templates/stock/item_base.html:123 +#: stock/templates/stock/item_base.html:131 msgid "Delete stock item" msgstr "" -#: stock/templates/stock/item_base.html:127 +#: stock/templates/stock/item_base.html:135 msgid "Generate test report" msgstr "" -#: stock/templates/stock/item_base.html:135 +#: stock/templates/stock/item_base.html:143 msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:168 -msgid "Belongs To" -msgstr "" - -#: stock/templates/stock/item_base.html:190 +#: stock/templates/stock/item_base.html:202 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:197 +#: stock/templates/stock/item_base.html:209 msgid "Unique Identifier" msgstr "" -#: stock/templates/stock/item_base.html:225 +#: stock/templates/stock/item_base.html:237 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:250 +#: stock/templates/stock/item_base.html:262 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:255 +#: stock/templates/stock/item_base.html:267 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:259 +#: stock/templates/stock/item_base.html:271 msgid "No stocktake performed" msgstr "" @@ -3423,29 +3472,32 @@ msgstr "" msgid "Are you sure you want to delete this stock item?" msgstr "" +#: stock/templates/stock/item_install.html:7 +msgid "Install another StockItem into this item." +msgstr "" + +#: stock/templates/stock/item_install.html:10 +msgid "Stock items can only be installed if they meet the following criteria" +msgstr "" + +#: stock/templates/stock/item_install.html:13 +msgid "The StockItem links to a Part which is in the BOM for this StockItem" +msgstr "" + +#: stock/templates/stock/item_install.html:14 +msgid "The StockItem is currently in stock" +msgstr "" + #: stock/templates/stock/item_installed.html:10 msgid "Installed Stock Items" msgstr "" -#: stock/templates/stock/item_installed.html:18 -msgid "Uninstall selected stock items" +#: stock/templates/stock/item_serialize.html:5 +msgid "Create serialized items from this stock item." msgstr "" -#: stock/templates/stock/item_installed.html:18 -msgid "Uninstall" -msgstr "" - -#: stock/templates/stock/item_installed.html:35 -msgid "No stock items installed" -msgstr "" - -#: stock/templates/stock/item_installed.html:48 templates/js/part.html:209 -#: templates/js/stock.html:409 -msgid "Select" -msgstr "" - -#: stock/templates/stock/item_installed.html:131 -msgid "Uninstall item" +#: stock/templates/stock/item_serialize.html:7 +msgid "Select quantity to serialize, and unique serial numbers." msgstr "" #: stock/templates/stock/item_tests.html:10 stock/templates/stock/tabs.html:13 @@ -3464,54 +3516,54 @@ msgstr "" msgid "Test Report" msgstr "" -#: stock/templates/stock/location.html:13 +#: stock/templates/stock/location.html:18 msgid "All stock items" msgstr "" -#: stock/templates/stock/location.html:26 +#: stock/templates/stock/location.html:31 msgid "Check-in Items" msgstr "" -#: stock/templates/stock/location.html:37 +#: stock/templates/stock/location.html:42 msgid "Location actions" msgstr "" -#: stock/templates/stock/location.html:39 +#: stock/templates/stock/location.html:44 msgid "Edit location" msgstr "" -#: stock/templates/stock/location.html:40 +#: stock/templates/stock/location.html:45 msgid "Delete location" msgstr "" -#: stock/templates/stock/location.html:48 +#: stock/templates/stock/location.html:53 msgid "Location Details" msgstr "" -#: stock/templates/stock/location.html:53 +#: stock/templates/stock/location.html:58 msgid "Location Path" msgstr "" -#: stock/templates/stock/location.html:58 +#: stock/templates/stock/location.html:63 msgid "Location Description" msgstr "" -#: stock/templates/stock/location.html:63 +#: stock/templates/stock/location.html:68 msgid "Sublocations" msgstr "" -#: stock/templates/stock/location.html:68 -#: stock/templates/stock/location.html:83 +#: stock/templates/stock/location.html:73 +#: stock/templates/stock/location.html:88 #: templates/InvenTree/search_stock_items.html:6 templates/stats.html:21 #: templates/stats.html:30 msgid "Stock Items" msgstr "" -#: stock/templates/stock/location.html:73 +#: stock/templates/stock/location.html:78 msgid "Stock Details" msgstr "" -#: stock/templates/stock/location.html:78 +#: stock/templates/stock/location.html:83 #: templates/InvenTree/search_stock_location.html:6 templates/stats.html:25 msgid "Stock Locations" msgstr "" @@ -3524,7 +3576,7 @@ msgstr "" msgid "The following stock items will be uninstalled" msgstr "" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1186 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1287 msgid "Convert Stock Item" msgstr "" @@ -3636,130 +3688,134 @@ msgstr "" msgid "Stock Item QR Code" msgstr "" -#: stock/views.py:699 +#: stock/views.py:700 +msgid "Install Stock Item" +msgstr "" + +#: stock/views.py:799 msgid "Uninstall Stock Items" msgstr "" -#: stock/views.py:806 +#: stock/views.py:906 msgid "Uninstalled stock items" msgstr "" -#: stock/views.py:831 +#: stock/views.py:931 msgid "Adjust Stock" msgstr "" -#: stock/views.py:940 +#: stock/views.py:1040 msgid "Move Stock Items" msgstr "" -#: stock/views.py:941 +#: stock/views.py:1041 msgid "Count Stock Items" msgstr "" -#: stock/views.py:942 +#: stock/views.py:1042 msgid "Remove From Stock" msgstr "" -#: stock/views.py:943 +#: stock/views.py:1043 msgid "Add Stock Items" msgstr "" -#: stock/views.py:944 +#: stock/views.py:1044 msgid "Delete Stock Items" msgstr "" -#: stock/views.py:972 +#: stock/views.py:1072 msgid "Must enter integer value" msgstr "" -#: stock/views.py:977 +#: stock/views.py:1077 msgid "Quantity must be positive" msgstr "" -#: stock/views.py:984 +#: stock/views.py:1084 #, python-brace-format msgid "Quantity must not exceed {x}" msgstr "" -#: stock/views.py:1063 +#: stock/views.py:1163 #, python-brace-format msgid "Added stock to {n} items" msgstr "" -#: stock/views.py:1078 +#: stock/views.py:1178 #, python-brace-format msgid "Removed stock from {n} items" msgstr "" -#: stock/views.py:1091 +#: stock/views.py:1191 #, python-brace-format msgid "Counted stock for {n} items" msgstr "" -#: stock/views.py:1119 +#: stock/views.py:1219 msgid "No items were moved" msgstr "" -#: stock/views.py:1122 +#: stock/views.py:1222 #, python-brace-format msgid "Moved {n} items to {dest}" msgstr "" -#: stock/views.py:1141 +#: stock/views.py:1241 #, python-brace-format msgid "Deleted {n} stock items" msgstr "" -#: stock/views.py:1153 +#: stock/views.py:1253 msgid "Edit Stock Item" msgstr "" -#: stock/views.py:1234 +#: stock/views.py:1335 msgid "Serialize Stock" msgstr "" -#: stock/views.py:1426 +#: stock/views.py:1527 msgid "Duplicate Stock Item" msgstr "" -#: stock/views.py:1492 +#: stock/views.py:1593 msgid "Invalid quantity" msgstr "" -#: stock/views.py:1495 +#: stock/views.py:1596 msgid "Quantity cannot be less than zero" msgstr "" -#: stock/views.py:1499 +#: stock/views.py:1600 msgid "Invalid part selection" msgstr "" -#: stock/views.py:1548 +#: stock/views.py:1649 #, python-brace-format msgid "Created {n} new stock items" msgstr "" -#: stock/views.py:1567 stock/views.py:1583 +#: stock/views.py:1668 stock/views.py:1684 msgid "Created new stock item" msgstr "" -#: stock/views.py:1602 +#: stock/views.py:1703 msgid "Delete Stock Location" msgstr "" -#: stock/views.py:1615 +#: stock/views.py:1716 msgid "Delete Stock Item" msgstr "" -#: stock/views.py:1626 +#: stock/views.py:1727 msgid "Delete Stock Tracking Entry" msgstr "" -#: stock/views.py:1643 +#: stock/views.py:1744 msgid "Edit Stock Tracking Entry" msgstr "" -#: stock/views.py:1652 +#: stock/views.py:1753 msgid "Add Stock Tracking Entry" msgstr "" @@ -3787,15 +3843,19 @@ msgstr "" msgid "Search Results" msgstr "" -#: templates/InvenTree/search.html:22 -msgid "No results found" +#: templates/InvenTree/search.html:24 +msgid "No results found for " msgstr "" -#: templates/InvenTree/search.html:181 templates/js/stock.html:521 +#: templates/InvenTree/search.html:42 +msgid "Enter a search query" +msgstr "" + +#: templates/InvenTree/search.html:191 templates/js/stock.html:527 msgid "Shipped to customer" msgstr "" -#: templates/InvenTree/search.html:184 templates/js/stock.html:528 +#: templates/InvenTree/search.html:194 templates/js/stock.html:537 msgid "No stock location set" msgstr "" @@ -3974,31 +4034,35 @@ msgstr "" msgid "Open subassembly" msgstr "" -#: templates/js/bom.html:184 templates/js/build.html:119 +#: templates/js/bom.html:173 +msgid "Optional" +msgstr "" + +#: templates/js/bom.html:188 templates/js/build.html:119 msgid "Available" msgstr "" -#: templates/js/bom.html:209 +#: templates/js/bom.html:213 msgid "No pricing available" msgstr "" -#: templates/js/bom.html:228 +#: templates/js/bom.html:232 msgid "Actions" msgstr "" -#: templates/js/bom.html:236 +#: templates/js/bom.html:240 msgid "Validate BOM Item" msgstr "" -#: templates/js/bom.html:238 +#: templates/js/bom.html:242 msgid "This line has been validated" msgstr "" -#: templates/js/bom.html:240 +#: templates/js/bom.html:244 msgid "Edit BOM Item" msgstr "" -#: templates/js/bom.html:242 +#: templates/js/bom.html:246 msgid "Delete BOM Item" msgstr "" @@ -4026,11 +4090,11 @@ msgstr "" msgid "No supplier parts found" msgstr "" -#: templates/js/company.html:145 templates/js/part.html:248 +#: templates/js/company.html:145 templates/js/part.html:314 msgid "Template part" msgstr "" -#: templates/js/company.html:149 templates/js/part.html:252 +#: templates/js/company.html:149 templates/js/part.html:318 msgid "Assembled part" msgstr "" @@ -4042,7 +4106,7 @@ msgstr "" msgid "No purchase orders found" msgstr "" -#: templates/js/order.html:172 templates/js/stock.html:633 +#: templates/js/order.html:172 templates/js/stock.html:642 msgid "Date" msgstr "" @@ -4058,51 +4122,56 @@ msgstr "" msgid "No variants found" msgstr "" -#: templates/js/part.html:256 -msgid "Starred part" -msgstr "" - -#: templates/js/part.html:260 -msgid "Salable part" -msgstr "" - -#: templates/js/part.html:299 -msgid "No category" -msgstr "" - -#: templates/js/part.html:317 templates/js/table_filters.html:191 -msgid "Low stock" -msgstr "" - -#: templates/js/part.html:326 -msgid "Building" -msgstr "" - -#: templates/js/part.html:345 +#: templates/js/part.html:223 templates/js/part.html:411 msgid "No parts found" msgstr "" -#: templates/js/part.html:405 +#: templates/js/part.html:275 templates/js/stock.html:409 +#: templates/js/stock.html:965 +msgid "Select" +msgstr "" + +#: templates/js/part.html:322 +msgid "Starred part" +msgstr "" + +#: templates/js/part.html:326 +msgid "Salable part" +msgstr "" + +#: templates/js/part.html:365 +msgid "No category" +msgstr "" + +#: templates/js/part.html:383 templates/js/table_filters.html:196 +msgid "Low stock" +msgstr "" + +#: templates/js/part.html:392 +msgid "Building" +msgstr "" + +#: templates/js/part.html:471 msgid "YES" msgstr "" -#: templates/js/part.html:407 +#: templates/js/part.html:473 msgid "NO" msgstr "" -#: templates/js/part.html:441 +#: templates/js/part.html:507 msgid "No test templates matching query" msgstr "" -#: templates/js/part.html:492 templates/js/stock.html:63 +#: templates/js/part.html:558 templates/js/stock.html:63 msgid "Edit test result" msgstr "" -#: templates/js/part.html:493 templates/js/stock.html:64 +#: templates/js/part.html:559 templates/js/stock.html:64 msgid "Delete test result" msgstr "" -#: templates/js/part.html:499 +#: templates/js/part.html:565 msgid "This test is defined for a parent part" msgstr "" @@ -4146,42 +4215,62 @@ msgstr "" msgid "Stock item has been assigned to customer" msgstr "" -#: templates/js/stock.html:474 +#: templates/js/stock.html:475 msgid "Stock item was assigned to a build order" msgstr "" -#: templates/js/stock.html:476 +#: templates/js/stock.html:477 msgid "Stock item was assigned to a sales order" msgstr "" -#: templates/js/stock.html:483 +#: templates/js/stock.html:482 +msgid "Stock item has been installed in another item" +msgstr "" + +#: templates/js/stock.html:489 msgid "Stock item has been rejected" msgstr "" -#: templates/js/stock.html:487 +#: templates/js/stock.html:493 msgid "Stock item is lost" msgstr "" -#: templates/js/stock.html:491 templates/js/table_filters.html:60 +#: templates/js/stock.html:497 templates/js/table_filters.html:60 msgid "Depleted" msgstr "" -#: templates/js/stock.html:516 +#: templates/js/stock.html:522 msgid "Installed in Stock Item " msgstr "" -#: templates/js/stock.html:699 +#: templates/js/stock.html:530 +msgid "Assigned to sales order" +msgstr "" + +#: templates/js/stock.html:708 msgid "No user information" msgstr "" -#: templates/js/stock.html:783 +#: templates/js/stock.html:792 msgid "Create New Part" msgstr "" -#: templates/js/stock.html:795 +#: templates/js/stock.html:804 msgid "Create New Location" msgstr "" +#: templates/js/stock.html:903 +msgid "Serial" +msgstr "" + +#: templates/js/stock.html:996 templates/js/table_filters.html:70 +msgid "Installed" +msgstr "" + +#: templates/js/stock.html:1021 +msgid "Install item" +msgstr "" + #: templates/js/table_filters.html:19 templates/js/table_filters.html:80 msgid "Is Serialized" msgstr "" @@ -4243,10 +4332,6 @@ msgstr "" msgid "Show items which are in stock" msgstr "" -#: templates/js/table_filters.html:70 -msgid "Installed" -msgstr "" - #: templates/js/table_filters.html:71 msgid "Show stock items which are installed in another item" msgstr "" @@ -4283,19 +4368,27 @@ msgstr "" msgid "Include parts in subcategories" msgstr "" +#: templates/js/table_filters.html:178 +msgid "Has IPN" +msgstr "" + #: templates/js/table_filters.html:179 +msgid "Part has internal part number" +msgstr "" + +#: templates/js/table_filters.html:184 msgid "Show active parts" msgstr "" -#: templates/js/table_filters.html:187 +#: templates/js/table_filters.html:192 msgid "Stock available" msgstr "" -#: templates/js/table_filters.html:203 +#: templates/js/table_filters.html:208 msgid "Starred" msgstr "" -#: templates/js/table_filters.html:215 +#: templates/js/table_filters.html:220 msgid "Purchasable" msgstr "" @@ -4339,42 +4432,42 @@ msgstr "" msgid "Search" msgstr "" -#: templates/stock_table.html:5 +#: templates/stock_table.html:6 msgid "Export Stock Information" msgstr "" -#: templates/stock_table.html:12 +#: templates/stock_table.html:13 msgid "Add to selected stock items" msgstr "" -#: templates/stock_table.html:13 +#: templates/stock_table.html:14 msgid "Remove from selected stock items" msgstr "" -#: templates/stock_table.html:14 +#: templates/stock_table.html:15 msgid "Stocktake selected stock items" msgstr "" -#: templates/stock_table.html:15 +#: templates/stock_table.html:16 msgid "Move selected stock items" msgstr "" -#: templates/stock_table.html:15 +#: templates/stock_table.html:16 msgid "Move stock" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:17 msgid "Order selected items" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:17 msgid "Order stock" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:18 msgid "Delete selected items" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:18 msgid "Delete Stock" msgstr "" diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po index d4a3d7ceb6..0b5425db6e 100644 --- a/InvenTree/locale/es/LC_MESSAGES/django.po +++ b/InvenTree/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-09-28 12:03+0000\n" +"POT-Creation-Date: 2020-10-04 14:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -86,7 +86,7 @@ msgstr "" msgid "File comment" msgstr "" -#: InvenTree/models.py:68 templates/js/stock.html:690 +#: InvenTree/models.py:68 templates/js/stock.html:699 msgid "User" msgstr "" @@ -99,19 +99,19 @@ msgstr "" msgid "Description (optional)" msgstr "" -#: InvenTree/settings.py:341 +#: InvenTree/settings.py:342 msgid "English" msgstr "" -#: InvenTree/settings.py:342 +#: InvenTree/settings.py:343 msgid "German" msgstr "" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:344 msgid "French" msgstr "" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:345 msgid "Polish" msgstr "" @@ -143,7 +143,8 @@ msgstr "" msgid "Returned" msgstr "" -#: InvenTree/status_codes.py:136 order/templates/order/sales_order_base.html:98 +#: InvenTree/status_codes.py:136 +#: order/templates/order/sales_order_base.html:103 msgid "Shipped" msgstr "" @@ -198,7 +199,7 @@ msgstr "" msgid "Overage must be an integer value or a percentage" msgstr "" -#: InvenTree/views.py:639 +#: InvenTree/views.py:661 msgid "Database Statistics" msgstr "" @@ -250,7 +251,7 @@ msgstr "" msgid "Serial numbers" msgstr "" -#: build/forms.py:64 stock/forms.py:107 +#: build/forms.py:64 stock/forms.py:111 msgid "Enter unique serial numbers (or leave blank)" msgstr "" @@ -262,7 +263,7 @@ msgstr "" msgid "Build quantity must be integer value for trackable parts" msgstr "" -#: build/models.py:73 build/templates/build/build_base.html:65 +#: build/models.py:73 build/templates/build/build_base.html:70 msgid "Build Title" msgstr "" @@ -270,7 +271,7 @@ msgstr "" msgid "Brief description of the build" msgstr "" -#: build/models.py:84 build/templates/build/build_base.html:86 +#: build/models.py:84 build/templates/build/build_base.html:91 msgid "Parent Build" msgstr "" @@ -280,18 +281,17 @@ msgstr "" #: build/models.py:90 build/templates/build/allocate.html:329 #: build/templates/build/auto_allocate.html:19 -#: build/templates/build/build_base.html:70 +#: build/templates/build/build_base.html:75 #: build/templates/build/detail.html:22 order/models.py:501 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:147 -#: order/templates/order/receive_parts.html:19 part/models.py:241 +#: order/templates/order/receive_parts.html:19 part/models.py:293 #: part/templates/part/part_app_base.html:7 -#: part/templates/part/set_category.html:13 -#: stock/templates/stock/item_installed.html:60 -#: templates/InvenTree/search.html:123 templates/js/barcode.html:336 -#: templates/js/bom.html:124 templates/js/build.html:47 -#: templates/js/company.html:137 templates/js/part.html:223 -#: templates/js/stock.html:421 +#: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 +#: templates/js/barcode.html:336 templates/js/bom.html:124 +#: templates/js/build.html:47 templates/js/company.html:137 +#: templates/js/part.html:184 templates/js/part.html:289 +#: templates/js/stock.html:421 templates/js/stock.html:977 msgid "Part" msgstr "" @@ -325,7 +325,7 @@ msgstr "" msgid "Number of parts to build" msgstr "" -#: build/models.py:128 part/templates/part/part_base.html:142 +#: build/models.py:128 part/templates/part/part_base.html:145 msgid "Build Status" msgstr "" @@ -333,7 +333,7 @@ msgstr "" msgid "Build status code" msgstr "" -#: build/models.py:136 stock/models.py:371 +#: build/models.py:136 stock/models.py:387 msgid "Batch Code" msgstr "" @@ -344,12 +344,12 @@ msgstr "" #: build/models.py:155 build/templates/build/detail.html:55 #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 -#: part/templates/part/detail.html:80 part/templates/part/part_base.html:89 -#: stock/models.py:365 stock/templates/stock/item_base.html:232 +#: part/templates/part/detail.html:80 part/templates/part/part_base.html:92 +#: stock/models.py:381 stock/templates/stock/item_base.html:244 msgid "External Link" msgstr "" -#: build/models.py:156 stock/models.py:367 +#: build/models.py:156 stock/models.py:383 msgid "Link to external URL" msgstr "" @@ -357,10 +357,10 @@ msgstr "" #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:15 #: order/templates/order/purchase_order_detail.html:202 #: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:67 -#: stock/forms.py:281 stock/forms.py:309 stock/models.py:433 -#: stock/models.py:1353 stock/templates/stock/tabs.html:26 -#: templates/js/barcode.html:391 templates/js/bom.html:219 -#: templates/js/stock.html:116 templates/js/stock.html:534 +#: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 +#: stock/models.py:1404 stock/templates/stock/tabs.html:26 +#: templates/js/barcode.html:391 templates/js/bom.html:223 +#: templates/js/stock.html:116 templates/js/stock.html:543 msgid "Notes" msgstr "" @@ -404,7 +404,7 @@ msgstr "" #: build/templates/build/allocate.html:17 #: company/templates/company/detail_part.html:18 order/views.py:779 -#: part/templates/part/category.html:107 +#: part/templates/part/category.html:112 msgid "Order Parts" msgstr "" @@ -420,24 +420,24 @@ msgstr "" msgid "Unallocate" msgstr "" -#: build/templates/build/allocate.html:87 templates/stock_table.html:8 +#: build/templates/build/allocate.html:87 templates/stock_table.html:9 msgid "New Stock Item" msgstr "" -#: build/templates/build/allocate.html:88 stock/views.py:1327 +#: build/templates/build/allocate.html:88 stock/views.py:1428 msgid "Create new Stock Item" msgstr "" #: build/templates/build/allocate.html:170 #: order/templates/order/sales_order_detail.html:68 -#: order/templates/order/sales_order_detail.html:150 stock/models.py:359 -#: stock/templates/stock/item_base.html:148 +#: order/templates/order/sales_order_detail.html:150 stock/models.py:375 +#: stock/templates/stock/item_base.html:156 msgid "Serial Number" msgstr "" #: build/templates/build/allocate.html:172 #: build/templates/build/auto_allocate.html:20 -#: build/templates/build/build_base.html:75 +#: build/templates/build/build_base.html:80 #: build/templates/build/detail.html:27 #: company/templates/company/supplier_part_pricing.html:71 #: order/templates/order/order_wizard/select_parts.html:32 @@ -446,22 +446,22 @@ msgstr "" #: order/templates/order/sales_order_detail.html:152 #: part/templates/part/allocation.html:16 #: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:80 +#: part/templates/part/sale_prices.html:80 stock/forms.py:297 #: stock/templates/stock/item_base.html:26 #: stock/templates/stock/item_base.html:32 -#: stock/templates/stock/item_base.html:154 +#: stock/templates/stock/item_base.html:162 #: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.html:338 #: templates/js/bom.html:162 templates/js/build.html:58 -#: templates/js/stock.html:681 +#: templates/js/stock.html:690 templates/js/stock.html:905 msgid "Quantity" msgstr "" #: build/templates/build/allocate.html:186 -#: build/templates/build/auto_allocate.html:21 stock/forms.py:279 -#: stock/templates/stock/item_base.html:186 +#: build/templates/build/auto_allocate.html:21 stock/forms.py:336 +#: stock/templates/stock/item_base.html:198 #: stock/templates/stock/stock_adjust.html:17 -#: templates/InvenTree/search.html:173 templates/js/barcode.html:337 -#: templates/js/stock.html:512 +#: templates/InvenTree/search.html:183 templates/js/barcode.html:337 +#: templates/js/stock.html:518 msgid "Location" msgstr "" @@ -475,7 +475,7 @@ msgstr "" msgid "Delete stock allocation" msgstr "" -#: build/templates/build/allocate.html:238 templates/js/bom.html:330 +#: build/templates/build/allocate.html:238 templates/js/bom.html:334 msgid "No BOM items found" msgstr "" @@ -484,12 +484,12 @@ msgstr "" #: company/templates/company/supplier_part_detail.html:27 #: order/templates/order/purchase_order_detail.html:159 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 -#: stock/templates/stock/item_installed.html:83 -#: templates/InvenTree/search.html:137 templates/js/bom.html:147 +#: templates/InvenTree/search.html:147 templates/js/bom.html:147 #: templates/js/company.html:56 templates/js/order.html:159 #: templates/js/order.html:234 templates/js/part.html:120 -#: templates/js/part.html:279 templates/js/part.html:460 -#: templates/js/stock.html:444 templates/js/stock.html:662 +#: templates/js/part.html:203 templates/js/part.html:345 +#: templates/js/part.html:526 templates/js/stock.html:444 +#: templates/js/stock.html:671 msgid "Description" msgstr "" @@ -499,8 +499,8 @@ msgstr "" msgid "Reference" msgstr "" -#: build/templates/build/allocate.html:347 part/models.py:1348 -#: templates/js/part.html:464 templates/js/table_filters.html:121 +#: build/templates/build/allocate.html:347 part/models.py:1401 +#: templates/js/part.html:530 templates/js/table_filters.html:121 msgid "Required" msgstr "" @@ -547,7 +547,7 @@ msgstr "" #: build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 -#: stock/templates/stock/item_base.html:211 templates/js/build.html:39 +#: stock/templates/stock/item_base.html:223 templates/js/build.html:39 #: templates/navbar.html:20 msgid "Build" msgstr "" @@ -560,40 +560,49 @@ msgstr "" msgid "This build is a child of Build" msgstr "" -#: build/templates/build/build_base.html:61 build/templates/build/detail.html:9 +#: build/templates/build/build_base.html:39 +#: company/templates/company/company_base.html:27 +#: order/templates/order/order_base.html:28 +#: order/templates/order/sales_order_base.html:38 +#: part/templates/part/category.html:13 part/templates/part/part_base.html:32 +#: stock/templates/stock/item_base.html:69 +#: stock/templates/stock/location.html:12 +msgid "Admin view" +msgstr "" + +#: build/templates/build/build_base.html:66 build/templates/build/detail.html:9 msgid "Build Details" msgstr "" -#: build/templates/build/build_base.html:80 +#: build/templates/build/build_base.html:85 #: build/templates/build/detail.html:42 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:264 -#: stock/templates/stock/item_installed.html:111 -#: templates/InvenTree/search.html:165 templates/js/barcode.html:42 -#: templates/js/build.html:63 templates/js/order.html:164 -#: templates/js/order.html:239 templates/js/stock.html:499 +#: stock/templates/stock/item_base.html:276 templates/InvenTree/search.html:175 +#: templates/js/barcode.html:42 templates/js/build.html:63 +#: templates/js/order.html:164 templates/js/order.html:239 +#: templates/js/stock.html:505 templates/js/stock.html:913 msgid "Status" msgstr "" -#: build/templates/build/build_base.html:93 order/models.py:499 +#: build/templates/build/build_base.html:98 order/models.py:499 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:174 templates/js/order.html:213 +#: stock/templates/stock/item_base.html:186 templates/js/order.html:213 msgid "Sales Order" msgstr "" -#: build/templates/build/build_base.html:99 +#: build/templates/build/build_base.html:104 msgid "BOM Price" msgstr "" -#: build/templates/build/build_base.html:104 +#: build/templates/build/build_base.html:109 msgid "BOM pricing is incomplete" msgstr "" -#: build/templates/build/build_base.html:107 +#: build/templates/build/build_base.html:112 msgid "No pricing information" msgstr "" @@ -648,15 +657,15 @@ msgid "Stock can be taken from any available location." msgstr "" #: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:204 -#: stock/templates/stock/item_installed.html:119 templates/js/stock.html:507 -#: templates/js/table_filters.html:34 templates/js/table_filters.html:100 +#: stock/templates/stock/item_base.html:216 templates/js/stock.html:513 +#: templates/js/stock.html:920 templates/js/table_filters.html:34 +#: templates/js/table_filters.html:100 msgid "Batch" msgstr "" #: build/templates/build/detail.html:61 -#: order/templates/order/order_base.html:93 -#: order/templates/order/sales_order_base.html:92 templates/js/build.html:71 +#: order/templates/order/order_base.html:98 +#: order/templates/order/sales_order_base.html:97 templates/js/build.html:71 msgid "Created" msgstr "" @@ -769,7 +778,7 @@ msgstr "" msgid "Invalid location selected" msgstr "" -#: build/views.py:296 stock/views.py:1520 +#: build/views.py:296 stock/views.py:1621 #, python-brace-format msgid "The following serial numbers already exist: ({sn})" msgstr "" @@ -878,7 +887,7 @@ msgstr "" msgid "Description of the company" msgstr "" -#: company/models.py:91 company/templates/company/company_base.html:48 +#: company/models.py:91 company/templates/company/company_base.html:53 #: templates/js/company.html:61 msgid "Website" msgstr "" @@ -887,7 +896,7 @@ msgstr "" msgid "Company website URL" msgstr "" -#: company/models.py:94 company/templates/company/company_base.html:55 +#: company/models.py:94 company/templates/company/company_base.html:60 msgid "Address" msgstr "" @@ -903,7 +912,7 @@ msgstr "" msgid "Contact phone number" msgstr "" -#: company/models.py:101 company/templates/company/company_base.html:69 +#: company/models.py:101 company/templates/company/company_base.html:74 msgid "Email" msgstr "" @@ -911,7 +920,7 @@ msgstr "" msgid "Contact email address" msgstr "" -#: company/models.py:104 company/templates/company/company_base.html:76 +#: company/models.py:104 company/templates/company/company_base.html:81 msgid "Contact" msgstr "" @@ -935,8 +944,8 @@ msgstr "" msgid "Does this company manufacture parts?" msgstr "" -#: company/models.py:279 stock/models.py:319 -#: stock/templates/stock/item_base.html:140 +#: company/models.py:279 stock/models.py:335 +#: stock/templates/stock/item_base.html:148 msgid "Base Part" msgstr "" @@ -986,12 +995,12 @@ msgstr "" msgid "Company" msgstr "" -#: company/templates/company/company_base.html:42 +#: company/templates/company/company_base.html:47 #: company/templates/company/detail.html:8 msgid "Company Details" msgstr "" -#: company/templates/company/company_base.html:62 +#: company/templates/company/company_base.html:67 msgid "Phone" msgstr "" @@ -1005,16 +1014,16 @@ msgstr "" #: company/templates/company/detail.html:21 #: company/templates/company/supplier_part_base.html:66 #: company/templates/company/supplier_part_detail.html:21 -#: order/templates/order/order_base.html:74 +#: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:239 templates/js/company.html:48 +#: stock/templates/stock/item_base.html:251 templates/js/company.html:48 #: templates/js/company.html:162 templates/js/order.html:146 msgid "Supplier" msgstr "" #: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:73 stock/models.py:354 -#: stock/models.py:355 stock/templates/stock/item_base.html:161 +#: order/templates/order/sales_order_base.html:78 stock/models.py:370 +#: stock/models.py:371 stock/templates/stock/item_base.html:169 #: templates/js/company.html:40 templates/js/order.html:221 msgid "Customer" msgstr "" @@ -1030,18 +1039,18 @@ msgstr "" #: company/templates/company/detail_part.html:13 #: order/templates/order/purchase_order_detail.html:67 -#: part/templates/part/supplier.html:13 templates/js/stock.html:788 +#: part/templates/part/supplier.html:13 templates/js/stock.html:797 msgid "New Supplier Part" msgstr "" #: company/templates/company/detail_part.html:15 -#: part/templates/part/category.html:104 part/templates/part/supplier.html:15 -#: stock/templates/stock/item_installed.html:16 templates/stock_table.html:10 +#: part/templates/part/category.html:109 part/templates/part/supplier.html:15 +#: templates/stock_table.html:11 msgid "Options" msgstr "" #: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:107 +#: part/templates/part/category.html:112 msgid "Order parts" msgstr "" @@ -1054,7 +1063,7 @@ msgid "Delete Parts" msgstr "" #: company/templates/company/detail_part.html:43 -#: part/templates/part/category.html:102 templates/js/stock.html:782 +#: part/templates/part/category.html:107 templates/js/stock.html:791 msgid "New Part" msgstr "" @@ -1086,8 +1095,8 @@ msgstr "" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:101 part/templates/part/category.html:108 -#: part/templates/part/stock.html:51 templates/stock_table.html:5 +#: part/templates/part/category.html:106 part/templates/part/category.html:113 +#: part/templates/part/stock.html:51 templates/stock_table.html:6 msgid "Export" msgstr "" @@ -1143,8 +1152,8 @@ msgid "New Sales Order" msgstr "" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:328 -#: stock/templates/stock/item_base.html:244 templates/js/company.html:178 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:344 +#: stock/templates/stock/item_base.html:256 templates/js/company.html:178 msgid "Supplier Part" msgstr "" @@ -1196,7 +1205,7 @@ msgid "Pricing Information" msgstr "" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2108 +#: part/templates/part/sale_prices.html:13 part/views.py:2149 msgid "Add Price Break" msgstr "" @@ -1206,7 +1215,7 @@ msgid "No price break information found" msgstr "" #: company/templates/company/supplier_part_pricing.html:76 -#: part/templates/part/sale_prices.html:85 templates/js/bom.html:203 +#: part/templates/part/sale_prices.html:85 templates/js/bom.html:207 msgid "Price" msgstr "" @@ -1230,9 +1239,8 @@ msgstr "" #: company/templates/company/supplier_part_tabs.html:8 #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 -#: stock/templates/stock/item_installed.html:91 -#: stock/templates/stock/location.html:12 templates/InvenTree/search.html:145 -#: templates/js/part.html:124 templates/js/part.html:306 +#: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 +#: templates/js/part.html:124 templates/js/part.html:372 #: templates/js/stock.html:452 templates/navbar.html:19 msgid "Stock" msgstr "" @@ -1242,9 +1250,10 @@ msgid "Orders" msgstr "" #: company/templates/company/tabs.html:9 -#: order/templates/order/receive_parts.html:14 part/models.py:242 -#: part/templates/part/cat_link.html:7 part/templates/part/category.html:83 -#: templates/navbar.html:18 templates/stats.html:8 templates/stats.html:17 +#: order/templates/order/receive_parts.html:14 part/models.py:294 +#: part/templates/part/cat_link.html:7 part/templates/part/category.html:88 +#: part/templates/part/category_tabs.html:6 templates/navbar.html:18 +#: templates/stats.html:8 templates/stats.html:17 msgid "Parts" msgstr "" @@ -1313,7 +1322,7 @@ msgstr "" msgid "Edit Supplier Part" msgstr "" -#: company/views.py:269 templates/js/stock.html:789 +#: company/views.py:269 templates/js/stock.html:798 msgid "Create new Supplier Part" msgstr "" @@ -1321,15 +1330,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:404 part/views.py:2112 +#: company/views.py:404 part/views.py:2153 msgid "Added new price break" msgstr "" -#: company/views.py:441 part/views.py:2157 +#: company/views.py:441 part/views.py:2198 msgid "Edit Price Break" msgstr "" -#: company/views.py:456 part/views.py:2171 +#: company/views.py:456 part/views.py:2212 msgid "Delete Price Break" msgstr "" @@ -1366,11 +1375,11 @@ msgid "Mark order as complete" msgstr "" #: order/forms.py:46 order/forms.py:57 -#: order/templates/order/sales_order_base.html:49 +#: order/templates/order/sales_order_base.html:54 msgid "Cancel order" msgstr "" -#: order/forms.py:68 order/templates/order/sales_order_base.html:46 +#: order/forms.py:68 order/templates/order/sales_order_base.html:51 msgid "Ship order" msgstr "" @@ -1423,7 +1432,7 @@ msgid "Date order was completed" msgstr "" #: order/models.py:185 order/models.py:259 part/views.py:1304 -#: stock/models.py:239 stock/models.py:754 +#: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "" @@ -1461,7 +1470,7 @@ msgstr "" #: order/models.py:466 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:23 -#: stock/templates/stock/item_base.html:218 templates/js/order.html:138 +#: stock/templates/stock/item_base.html:230 templates/js/order.html:138 msgid "Purchase Order" msgstr "" @@ -1503,32 +1512,32 @@ msgstr "" msgid "Are you sure you want to delete this attachment?" msgstr "" -#: order/templates/order/order_base.html:59 -msgid "Purchase Order Details" -msgstr "" - #: order/templates/order/order_base.html:64 -#: order/templates/order/sales_order_base.html:63 -msgid "Order Reference" +msgid "Purchase Order Details" msgstr "" #: order/templates/order/order_base.html:69 #: order/templates/order/sales_order_base.html:68 +msgid "Order Reference" +msgstr "" + +#: order/templates/order/order_base.html:74 +#: order/templates/order/sales_order_base.html:73 msgid "Order Status" msgstr "" -#: order/templates/order/order_base.html:80 templates/js/order.html:153 +#: order/templates/order/order_base.html:85 templates/js/order.html:153 msgid "Supplier Reference" msgstr "" -#: order/templates/order/order_base.html:99 +#: order/templates/order/order_base.html:104 msgid "Issued" msgstr "" -#: order/templates/order/order_base.html:106 +#: order/templates/order/order_base.html:111 #: order/templates/order/purchase_order_detail.html:182 #: order/templates/order/receive_parts.html:22 -#: order/templates/order/sales_order_base.html:105 +#: order/templates/order/sales_order_base.html:110 msgid "Received" msgstr "" @@ -1607,14 +1616,14 @@ msgstr "" #: order/templates/order/purchase_order_detail.html:38 #: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:153 part/templates/part/category.html:194 -#: templates/js/stock.html:794 +#: part/templates/part/category.html:161 part/templates/part/category.html:202 +#: templates/js/stock.html:803 msgid "New Location" msgstr "" #: order/templates/order/purchase_order_detail.html:39 #: order/templates/order/purchase_order_detail.html:119 -#: stock/templates/stock/location.html:16 +#: stock/templates/stock/location.html:21 msgid "Create new stock location" msgstr "" @@ -1649,7 +1658,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:132 templates/js/part.html:322 +#: part/templates/part/part_base.html:135 templates/js/part.html:388 msgid "On Order" msgstr "" @@ -1665,15 +1674,15 @@ msgstr "" msgid "This SalesOrder has not been fully allocated" msgstr "" -#: order/templates/order/sales_order_base.html:42 +#: order/templates/order/sales_order_base.html:47 msgid "Packing List" msgstr "" -#: order/templates/order/sales_order_base.html:58 +#: order/templates/order/sales_order_base.html:63 msgid "Sales Order Details" msgstr "" -#: order/templates/order/sales_order_base.html:79 templates/js/order.html:228 +#: order/templates/order/sales_order_base.html:84 templates/js/order.html:228 msgid "Customer Reference" msgstr "" @@ -1881,12 +1890,12 @@ msgstr "" msgid "Remove allocation" msgstr "" -#: part/bom.py:138 part/templates/part/category.html:50 +#: part/bom.py:138 part/templates/part/category.html:55 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "" -#: part/bom.py:139 part/templates/part/part_base.html:105 +#: part/bom.py:139 part/templates/part/part_base.html:108 msgid "Available Stock" msgstr "" @@ -1903,11 +1912,11 @@ msgstr "" msgid "Error reading BOM file (incorrect row size)" msgstr "" -#: part/forms.py:57 stock/forms.py:250 +#: part/forms.py:57 stock/forms.py:254 msgid "File Format" msgstr "" -#: part/forms.py:57 stock/forms.py:250 +#: part/forms.py:57 stock/forms.py:254 msgid "Select output file format" msgstr "" @@ -1983,11 +1992,11 @@ msgstr "" msgid "Confirm part creation" msgstr "" -#: part/forms.py:247 +#: part/forms.py:248 msgid "Input quantity for price calculation" msgstr "" -#: part/forms.py:250 +#: part/forms.py:251 msgid "Select currency for price calculation" msgstr "" @@ -2003,222 +2012,226 @@ msgstr "" msgid "Part Category" msgstr "" -#: part/models.py:76 part/templates/part/category.html:13 -#: part/templates/part/category.html:78 templates/stats.html:12 +#: part/models.py:76 part/templates/part/category.html:18 +#: part/templates/part/category.html:83 templates/stats.html:12 msgid "Part Categories" msgstr "" -#: part/models.py:293 part/models.py:303 +#: part/models.py:345 part/models.py:355 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "" -#: part/models.py:383 +#: part/models.py:435 msgid "Next available serial numbers are" msgstr "" -#: part/models.py:387 +#: part/models.py:439 msgid "Next available serial number is" msgstr "" -#: part/models.py:392 +#: part/models.py:444 msgid "Most recent serial number is" msgstr "" -#: part/models.py:470 +#: part/models.py:522 msgid "Part must be unique for name, IPN and revision" msgstr "" -#: part/models.py:485 part/templates/part/detail.html:19 +#: part/models.py:537 part/templates/part/detail.html:19 msgid "Part name" msgstr "" -#: part/models.py:489 +#: part/models.py:541 msgid "Is this part a template part?" msgstr "" -#: part/models.py:498 +#: part/models.py:550 msgid "Is this part a variant of another part?" msgstr "" -#: part/models.py:500 +#: part/models.py:552 msgid "Part description" msgstr "" -#: part/models.py:502 +#: part/models.py:554 msgid "Part keywords to improve visibility in search results" msgstr "" -#: part/models.py:507 +#: part/models.py:559 msgid "Part category" msgstr "" -#: part/models.py:509 +#: part/models.py:561 msgid "Internal Part Number" msgstr "" -#: part/models.py:511 +#: part/models.py:563 msgid "Part revision or version number" msgstr "" -#: part/models.py:513 +#: part/models.py:565 msgid "Link to extenal URL" msgstr "" -#: part/models.py:525 +#: part/models.py:577 msgid "Where is this item normally stored?" msgstr "" -#: part/models.py:569 +#: part/models.py:621 msgid "Default supplier part" msgstr "" -#: part/models.py:572 +#: part/models.py:624 msgid "Minimum allowed stock level" msgstr "" -#: part/models.py:574 +#: part/models.py:626 msgid "Stock keeping units for this part" msgstr "" -#: part/models.py:576 +#: part/models.py:628 msgid "Can this part be built from other parts?" msgstr "" -#: part/models.py:578 +#: part/models.py:630 msgid "Can this part be used to build other parts?" msgstr "" -#: part/models.py:580 +#: part/models.py:632 msgid "Does this part have tracking for unique items?" msgstr "" -#: part/models.py:582 +#: part/models.py:634 msgid "Can this part be purchased from external suppliers?" msgstr "" -#: part/models.py:584 +#: part/models.py:636 msgid "Can this part be sold to customers?" msgstr "" -#: part/models.py:586 +#: part/models.py:638 msgid "Is this part active?" msgstr "" -#: part/models.py:588 +#: part/models.py:640 msgid "Is this a virtual part, such as a software product or license?" msgstr "" -#: part/models.py:590 +#: part/models.py:642 msgid "Part notes - supports Markdown formatting" msgstr "" -#: part/models.py:592 +#: part/models.py:644 msgid "Stored BOM checksum" msgstr "" -#: part/models.py:1300 +#: part/models.py:1353 msgid "Test templates can only be created for trackable parts" msgstr "" -#: part/models.py:1317 +#: part/models.py:1370 msgid "Test with this name already exists for this part" msgstr "" -#: part/models.py:1336 templates/js/part.html:455 templates/js/stock.html:92 +#: part/models.py:1389 templates/js/part.html:521 templates/js/stock.html:92 msgid "Test Name" msgstr "" -#: part/models.py:1337 +#: part/models.py:1390 msgid "Enter a name for the test" msgstr "" -#: part/models.py:1342 +#: part/models.py:1395 msgid "Test Description" msgstr "" -#: part/models.py:1343 +#: part/models.py:1396 msgid "Enter description for this test" msgstr "" -#: part/models.py:1349 +#: part/models.py:1402 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1354 templates/js/part.html:472 +#: part/models.py:1407 templates/js/part.html:538 msgid "Requires Value" msgstr "" -#: part/models.py:1355 +#: part/models.py:1408 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1360 templates/js/part.html:479 +#: part/models.py:1413 templates/js/part.html:545 msgid "Requires Attachment" msgstr "" -#: part/models.py:1361 +#: part/models.py:1414 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1394 +#: part/models.py:1447 msgid "Parameter template name must be unique" msgstr "" -#: part/models.py:1399 +#: part/models.py:1452 msgid "Parameter Name" msgstr "" -#: part/models.py:1401 +#: part/models.py:1454 msgid "Parameter Units" msgstr "" -#: part/models.py:1427 +#: part/models.py:1480 msgid "Parent Part" msgstr "" -#: part/models.py:1429 +#: part/models.py:1482 msgid "Parameter Template" msgstr "" -#: part/models.py:1431 +#: part/models.py:1484 msgid "Parameter Value" msgstr "" -#: part/models.py:1467 +#: part/models.py:1521 msgid "Select parent part" msgstr "" -#: part/models.py:1475 +#: part/models.py:1529 msgid "Select part to be used in BOM" msgstr "" -#: part/models.py:1481 +#: part/models.py:1535 msgid "BOM quantity for this BOM item" msgstr "" -#: part/models.py:1484 +#: part/models.py:1537 +msgid "This BOM item is optional" +msgstr "" + +#: part/models.py:1540 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "" -#: part/models.py:1487 +#: part/models.py:1543 msgid "BOM item reference" msgstr "" -#: part/models.py:1490 +#: part/models.py:1546 msgid "BOM item notes" msgstr "" -#: part/models.py:1492 +#: part/models.py:1548 msgid "BOM line checksum" msgstr "" -#: part/models.py:1556 part/views.py:1310 part/views.py:1362 -#: stock/models.py:229 +#: part/models.py:1612 part/views.py:1310 part/views.py:1362 +#: stock/models.py:231 msgid "Quantity must be integer value for trackable parts" msgstr "" -#: part/models.py:1565 +#: part/models.py:1621 msgid "BOM Item" msgstr "" @@ -2237,14 +2250,14 @@ msgstr "" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:58 -#: stock/templates/stock/item_base.html:226 +#: stock/templates/stock/item_base.html:238 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.html:112 -#: templates/js/stock.html:651 +#: templates/js/stock.html:660 templates/js/stock.html:896 msgid "Stock Item" msgstr "" #: part/templates/part/allocation.html:20 -#: stock/templates/stock/item_base.html:180 +#: stock/templates/stock/item_base.html:192 msgid "Build Order" msgstr "" @@ -2360,91 +2373,95 @@ msgstr "" msgid "Each part must already exist in the database" msgstr "" -#: part/templates/part/category.html:14 +#: part/templates/part/category.html:19 msgid "All parts" msgstr "" -#: part/templates/part/category.html:18 part/views.py:1935 +#: part/templates/part/category.html:23 part/views.py:1976 msgid "Create new part category" msgstr "" -#: part/templates/part/category.html:22 +#: part/templates/part/category.html:27 msgid "Edit part category" msgstr "" -#: part/templates/part/category.html:25 +#: part/templates/part/category.html:30 msgid "Delete part category" msgstr "" -#: part/templates/part/category.html:34 part/templates/part/category.html:73 +#: part/templates/part/category.html:39 part/templates/part/category.html:78 msgid "Category Details" msgstr "" -#: part/templates/part/category.html:39 +#: part/templates/part/category.html:44 msgid "Category Path" msgstr "" -#: part/templates/part/category.html:44 +#: part/templates/part/category.html:49 msgid "Category Description" msgstr "" -#: part/templates/part/category.html:57 part/templates/part/detail.html:64 +#: part/templates/part/category.html:62 part/templates/part/detail.html:64 msgid "Keywords" msgstr "" -#: part/templates/part/category.html:63 +#: part/templates/part/category.html:68 msgid "Subcategories" msgstr "" -#: part/templates/part/category.html:68 +#: part/templates/part/category.html:73 msgid "Parts (Including subcategories)" msgstr "" -#: part/templates/part/category.html:101 +#: part/templates/part/category.html:106 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:102 part/views.py:491 +#: part/templates/part/category.html:107 part/views.py:491 msgid "Create new part" msgstr "" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:111 msgid "Set category" msgstr "" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:111 msgid "Set Category" msgstr "" -#: part/templates/part/category.html:108 +#: part/templates/part/category.html:113 msgid "Export Data" msgstr "" -#: part/templates/part/category.html:154 +#: part/templates/part/category.html:162 msgid "Create new location" msgstr "" -#: part/templates/part/category.html:159 part/templates/part/category.html:188 +#: part/templates/part/category.html:167 part/templates/part/category.html:196 msgid "New Category" msgstr "" -#: part/templates/part/category.html:160 +#: part/templates/part/category.html:168 msgid "Create new category" msgstr "" -#: part/templates/part/category.html:189 +#: part/templates/part/category.html:197 msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:195 stock/views.py:1213 +#: part/templates/part/category.html:203 stock/views.py:1314 msgid "Create new Stock Location" msgstr "" +#: part/templates/part/category_tabs.html:9 +msgid "Parametric Table" +msgstr "" + #: part/templates/part/detail.html:9 msgid "Part Details" msgstr "" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:82 +#: part/templates/part/detail.html:25 part/templates/part/part_base.html:85 #: templates/js/part.html:112 msgid "IPN" msgstr "" @@ -2466,7 +2483,7 @@ msgid "Variant Of" msgstr "" #: part/templates/part/detail.html:70 part/templates/part/set_category.html:15 -#: templates/js/part.html:293 +#: templates/js/part.html:359 msgid "Category" msgstr "" @@ -2506,8 +2523,8 @@ msgstr "" msgid "Part is not a virtual part" msgstr "" -#: part/templates/part/detail.html:145 stock/forms.py:244 -#: templates/js/table_filters.html:183 +#: part/templates/part/detail.html:145 stock/forms.py:248 +#: templates/js/table_filters.html:188 msgid "Template" msgstr "" @@ -2519,7 +2536,7 @@ msgstr "" msgid "Part is not a template part" msgstr "" -#: part/templates/part/detail.html:154 templates/js/table_filters.html:195 +#: part/templates/part/detail.html:154 templates/js/table_filters.html:200 msgid "Assembly" msgstr "" @@ -2531,7 +2548,7 @@ msgstr "" msgid "Part cannot be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:163 templates/js/table_filters.html:199 +#: part/templates/part/detail.html:163 templates/js/table_filters.html:204 msgid "Component" msgstr "" @@ -2543,7 +2560,7 @@ msgstr "" msgid "Part cannot be used in assemblies" msgstr "" -#: part/templates/part/detail.html:172 templates/js/table_filters.html:211 +#: part/templates/part/detail.html:172 templates/js/table_filters.html:216 msgid "Trackable" msgstr "" @@ -2563,7 +2580,7 @@ msgstr "" msgid "Part can be purchased from external suppliers" msgstr "" -#: part/templates/part/detail.html:190 templates/js/table_filters.html:207 +#: part/templates/part/detail.html:190 templates/js/table_filters.html:212 msgid "Salable" msgstr "" @@ -2575,7 +2592,7 @@ msgstr "" msgid "Part cannot be sold to customers" msgstr "" -#: part/templates/part/detail.html:199 templates/js/table_filters.html:178 +#: part/templates/part/detail.html:199 templates/js/table_filters.html:183 msgid "Active" msgstr "" @@ -2607,7 +2624,7 @@ msgstr "" msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:21 stock/models.py:1340 +#: part/templates/part/params.html:21 stock/models.py:1391 #: templates/js/stock.html:112 msgid "Value" msgstr "" @@ -2636,70 +2653,70 @@ msgstr "" msgid "This part is a variant of" msgstr "" -#: part/templates/part/part_base.html:33 templates/js/company.html:153 -#: templates/js/part.html:270 +#: part/templates/part/part_base.html:36 templates/js/company.html:153 +#: templates/js/part.html:336 msgid "Inactive" msgstr "" -#: part/templates/part/part_base.html:40 +#: part/templates/part/part_base.html:43 msgid "Star this part" msgstr "" -#: part/templates/part/part_base.html:46 -#: stock/templates/stock/item_base.html:78 -#: stock/templates/stock/location.html:22 -msgid "Barcode actions" -msgstr "" - -#: part/templates/part/part_base.html:48 -#: stock/templates/stock/item_base.html:80 -#: stock/templates/stock/location.html:24 -msgid "Show QR Code" -msgstr "" - #: part/templates/part/part_base.html:49 #: stock/templates/stock/item_base.html:81 -#: stock/templates/stock/location.html:25 +#: stock/templates/stock/location.html:27 +msgid "Barcode actions" +msgstr "" + +#: part/templates/part/part_base.html:51 +#: stock/templates/stock/item_base.html:83 +#: stock/templates/stock/location.html:29 +msgid "Show QR Code" +msgstr "" + +#: part/templates/part/part_base.html:52 +#: stock/templates/stock/item_base.html:84 +#: stock/templates/stock/location.html:30 msgid "Print Label" msgstr "" -#: part/templates/part/part_base.html:53 +#: part/templates/part/part_base.html:56 msgid "Show pricing information" msgstr "" -#: part/templates/part/part_base.html:67 +#: part/templates/part/part_base.html:70 msgid "Part actions" msgstr "" -#: part/templates/part/part_base.html:69 +#: part/templates/part/part_base.html:72 msgid "Duplicate part" msgstr "" -#: part/templates/part/part_base.html:70 +#: part/templates/part/part_base.html:73 msgid "Edit part" msgstr "" -#: part/templates/part/part_base.html:72 +#: part/templates/part/part_base.html:75 msgid "Delete part" msgstr "" -#: part/templates/part/part_base.html:111 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:114 templates/js/table_filters.html:65 msgid "In Stock" msgstr "" -#: part/templates/part/part_base.html:118 +#: part/templates/part/part_base.html:121 msgid "Allocated to Build Orders" msgstr "" -#: part/templates/part/part_base.html:125 +#: part/templates/part/part_base.html:128 msgid "Allocated to Sales Orders" msgstr "" -#: part/templates/part/part_base.html:147 +#: part/templates/part/part_base.html:150 msgid "Can Build" msgstr "" -#: part/templates/part/part_base.html:153 +#: part/templates/part/part_base.html:156 msgid "Underway" msgstr "" @@ -2743,8 +2760,8 @@ msgstr "" msgid "Part Stock" msgstr "" -#: part/templates/part/stock_count.html:7 templates/js/bom.html:193 -#: templates/js/part.html:330 +#: part/templates/part/stock_count.html:7 templates/js/bom.html:197 +#: templates/js/part.html:396 msgid "No Stock" msgstr "" @@ -2784,7 +2801,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:270 +#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:282 msgid "Tests" msgstr "" @@ -2961,27 +2978,27 @@ msgstr "" msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1886 +#: part/views.py:1927 msgid "Edit Part Category" msgstr "" -#: part/views.py:1921 +#: part/views.py:1962 msgid "Delete Part Category" msgstr "" -#: part/views.py:1927 +#: part/views.py:1968 msgid "Part category was deleted" msgstr "" -#: part/views.py:1986 +#: part/views.py:2027 msgid "Create BOM item" msgstr "" -#: part/views.py:2052 +#: part/views.py:2093 msgid "Edit BOM item" msgstr "" -#: part/views.py:2100 +#: part/views.py:2141 msgid "Confim BOM item deletion" msgstr "" @@ -3013,267 +3030,295 @@ msgstr "" msgid "Asset file description" msgstr "" -#: stock/forms.py:187 +#: stock/forms.py:191 msgid "Label" msgstr "" -#: stock/forms.py:188 stock/forms.py:244 +#: stock/forms.py:192 stock/forms.py:248 msgid "Select test report template" msgstr "" -#: stock/forms.py:252 +#: stock/forms.py:256 msgid "Include stock items in sub locations" msgstr "" -#: stock/forms.py:279 +#: stock/forms.py:291 +msgid "Stock item to install" +msgstr "" + +#: stock/forms.py:298 +msgid "Stock quantity to assign" +msgstr "" + +#: stock/forms.py:326 +msgid "Must not exceed available quantity" +msgstr "" + +#: stock/forms.py:336 msgid "Destination location for uninstalled items" msgstr "" -#: stock/forms.py:281 +#: stock/forms.py:338 msgid "Add transaction note (optional)" msgstr "" -#: stock/forms.py:283 +#: stock/forms.py:340 msgid "Confirm uninstall" msgstr "" -#: stock/forms.py:283 +#: stock/forms.py:340 msgid "Confirm removal of installed stock items" msgstr "" -#: stock/forms.py:307 +#: stock/forms.py:364 msgid "Destination" msgstr "" -#: stock/forms.py:307 +#: stock/forms.py:364 msgid "Destination stock location" msgstr "" -#: stock/forms.py:309 +#: stock/forms.py:366 msgid "Add note (required)" msgstr "" -#: stock/forms.py:313 stock/views.py:795 stock/views.py:992 +#: stock/forms.py:370 stock/views.py:895 stock/views.py:1092 msgid "Confirm stock adjustment" msgstr "" -#: stock/forms.py:313 +#: stock/forms.py:370 msgid "Confirm movement of stock items" msgstr "" -#: stock/forms.py:315 +#: stock/forms.py:372 msgid "Set Default Location" msgstr "" -#: stock/forms.py:315 +#: stock/forms.py:372 msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:210 +#: stock/models.py:212 msgid "StockItem with this serial number already exists" msgstr "" -#: stock/models.py:246 +#: stock/models.py:248 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:256 stock/models.py:265 +#: stock/models.py:258 stock/models.py:267 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:257 +#: stock/models.py:259 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:278 +#: stock/models.py:281 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:311 +#: stock/models.py:287 +msgid "Item must have a build reference if is_building=True" +msgstr "" + +#: stock/models.py:294 +msgid "Build reference does not point to the same part object" +msgstr "" + +#: stock/models.py:327 msgid "Parent Stock Item" msgstr "" -#: stock/models.py:320 +#: stock/models.py:336 msgid "Base part" msgstr "" -#: stock/models.py:329 +#: stock/models.py:345 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:334 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:350 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "" -#: stock/models.py:337 +#: stock/models.py:353 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:342 +#: stock/models.py:358 stock/templates/stock/item_base.html:177 msgid "Installed In" msgstr "" -#: stock/models.py:345 +#: stock/models.py:361 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:361 +#: stock/models.py:377 msgid "Serial number for this item" msgstr "" -#: stock/models.py:373 +#: stock/models.py:389 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:377 +#: stock/models.py:393 msgid "Stock Quantity" msgstr "" -#: stock/models.py:386 +#: stock/models.py:402 msgid "Source Build" msgstr "" -#: stock/models.py:388 +#: stock/models.py:404 msgid "Build for this stock item" msgstr "" -#: stock/models.py:395 +#: stock/models.py:415 msgid "Source Purchase Order" msgstr "" -#: stock/models.py:398 +#: stock/models.py:418 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:404 +#: stock/models.py:424 msgid "Destination Sales Order" msgstr "" -#: stock/models.py:411 +#: stock/models.py:431 msgid "Destination Build Order" msgstr "" -#: stock/models.py:424 +#: stock/models.py:444 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:434 stock/templates/stock/item_notes.html:14 +#: stock/models.py:454 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:485 +#: stock/models.py:505 msgid "Assigned to Customer" msgstr "" -#: stock/models.py:487 +#: stock/models.py:507 msgid "Manually assigned to customer" msgstr "" -#: stock/models.py:500 +#: stock/models.py:520 msgid "Returned from customer" msgstr "" -#: stock/models.py:502 +#: stock/models.py:522 msgid "Returned to location" msgstr "" -#: stock/models.py:626 -msgid "Installed in stock item" +#: stock/models.py:650 +msgid "Installed into stock item" msgstr "" -#: stock/models.py:655 +#: stock/models.py:658 +msgid "Installed stock item" +msgstr "" + +#: stock/models.py:682 +msgid "Uninstalled stock item" +msgstr "" + +#: stock/models.py:701 msgid "Uninstalled into location" msgstr "" -#: stock/models.py:745 +#: stock/models.py:796 msgid "Part is not set as trackable" msgstr "" -#: stock/models.py:751 +#: stock/models.py:802 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:757 +#: stock/models.py:808 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:760 +#: stock/models.py:811 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:763 +#: stock/models.py:814 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:773 +#: stock/models.py:824 msgid "Serial numbers already exist: " msgstr "" -#: stock/models.py:798 +#: stock/models.py:849 msgid "Add serial number" msgstr "" -#: stock/models.py:801 +#: stock/models.py:852 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:912 +#: stock/models.py:963 msgid "StockItem cannot be moved as it is not in stock" msgstr "" -#: stock/models.py:1241 +#: stock/models.py:1292 msgid "Tracking entry title" msgstr "" -#: stock/models.py:1243 +#: stock/models.py:1294 msgid "Entry notes" msgstr "" -#: stock/models.py:1245 +#: stock/models.py:1296 msgid "Link to external page for further information" msgstr "" -#: stock/models.py:1305 +#: stock/models.py:1356 msgid "Value must be provided for this test" msgstr "" -#: stock/models.py:1311 +#: stock/models.py:1362 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1328 +#: stock/models.py:1379 msgid "Test" msgstr "" -#: stock/models.py:1329 +#: stock/models.py:1380 msgid "Test name" msgstr "" -#: stock/models.py:1334 +#: stock/models.py:1385 msgid "Result" msgstr "" -#: stock/models.py:1335 templates/js/table_filters.html:111 +#: stock/models.py:1386 templates/js/table_filters.html:111 msgid "Test result" msgstr "" -#: stock/models.py:1341 +#: stock/models.py:1392 msgid "Test output value" msgstr "" -#: stock/models.py:1347 +#: stock/models.py:1398 msgid "Attachment" msgstr "" -#: stock/models.py:1348 +#: stock/models.py:1399 msgid "Test result attachment" msgstr "" -#: stock/models.py:1354 +#: stock/models.py:1405 msgid "Test notes" msgstr "" @@ -3312,102 +3357,106 @@ msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" -#: stock/templates/stock/item_base.html:83 templates/js/barcode.html:283 +#: stock/templates/stock/item_base.html:86 templates/js/barcode.html:283 #: templates/js/barcode.html:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:85 +#: stock/templates/stock/item_base.html:88 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:91 +#: stock/templates/stock/item_base.html:94 msgid "Stock adjustment actions" msgstr "" -#: stock/templates/stock/item_base.html:95 -#: stock/templates/stock/location.html:33 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:98 +#: stock/templates/stock/location.html:38 templates/stock_table.html:15 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:96 templates/stock_table.html:12 +#: stock/templates/stock/item_base.html:99 templates/stock_table.html:13 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:97 templates/stock_table.html:13 +#: stock/templates/stock/item_base.html:100 templates/stock_table.html:14 msgid "Remove stock" msgstr "" -#: stock/templates/stock/item_base.html:99 +#: stock/templates/stock/item_base.html:102 msgid "Transfer stock" msgstr "" -#: stock/templates/stock/item_base.html:101 +#: stock/templates/stock/item_base.html:104 msgid "Serialize stock" msgstr "" -#: stock/templates/stock/item_base.html:105 +#: stock/templates/stock/item_base.html:108 msgid "Assign to customer" msgstr "" -#: stock/templates/stock/item_base.html:108 +#: stock/templates/stock/item_base.html:111 msgid "Return to stock" msgstr "" -#: stock/templates/stock/item_base.html:114 -#: stock/templates/stock/location.html:30 +#: stock/templates/stock/item_base.html:115 templates/js/stock.html:933 +msgid "Uninstall stock item" +msgstr "" + +#: stock/templates/stock/item_base.html:115 +msgid "Uninstall" +msgstr "" + +#: stock/templates/stock/item_base.html:122 +#: stock/templates/stock/location.html:35 msgid "Stock actions" msgstr "" -#: stock/templates/stock/item_base.html:118 +#: stock/templates/stock/item_base.html:126 msgid "Convert to variant" msgstr "" -#: stock/templates/stock/item_base.html:120 +#: stock/templates/stock/item_base.html:128 msgid "Duplicate stock item" msgstr "" -#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/item_base.html:129 msgid "Edit stock item" msgstr "" -#: stock/templates/stock/item_base.html:123 +#: stock/templates/stock/item_base.html:131 msgid "Delete stock item" msgstr "" -#: stock/templates/stock/item_base.html:127 +#: stock/templates/stock/item_base.html:135 msgid "Generate test report" msgstr "" -#: stock/templates/stock/item_base.html:135 +#: stock/templates/stock/item_base.html:143 msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:168 -msgid "Belongs To" -msgstr "" - -#: stock/templates/stock/item_base.html:190 +#: stock/templates/stock/item_base.html:202 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:197 +#: stock/templates/stock/item_base.html:209 msgid "Unique Identifier" msgstr "" -#: stock/templates/stock/item_base.html:225 +#: stock/templates/stock/item_base.html:237 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:250 +#: stock/templates/stock/item_base.html:262 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:255 +#: stock/templates/stock/item_base.html:267 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:259 +#: stock/templates/stock/item_base.html:271 msgid "No stocktake performed" msgstr "" @@ -3423,29 +3472,32 @@ msgstr "" msgid "Are you sure you want to delete this stock item?" msgstr "" +#: stock/templates/stock/item_install.html:7 +msgid "Install another StockItem into this item." +msgstr "" + +#: stock/templates/stock/item_install.html:10 +msgid "Stock items can only be installed if they meet the following criteria" +msgstr "" + +#: stock/templates/stock/item_install.html:13 +msgid "The StockItem links to a Part which is in the BOM for this StockItem" +msgstr "" + +#: stock/templates/stock/item_install.html:14 +msgid "The StockItem is currently in stock" +msgstr "" + #: stock/templates/stock/item_installed.html:10 msgid "Installed Stock Items" msgstr "" -#: stock/templates/stock/item_installed.html:18 -msgid "Uninstall selected stock items" +#: stock/templates/stock/item_serialize.html:5 +msgid "Create serialized items from this stock item." msgstr "" -#: stock/templates/stock/item_installed.html:18 -msgid "Uninstall" -msgstr "" - -#: stock/templates/stock/item_installed.html:35 -msgid "No stock items installed" -msgstr "" - -#: stock/templates/stock/item_installed.html:48 templates/js/part.html:209 -#: templates/js/stock.html:409 -msgid "Select" -msgstr "" - -#: stock/templates/stock/item_installed.html:131 -msgid "Uninstall item" +#: stock/templates/stock/item_serialize.html:7 +msgid "Select quantity to serialize, and unique serial numbers." msgstr "" #: stock/templates/stock/item_tests.html:10 stock/templates/stock/tabs.html:13 @@ -3464,54 +3516,54 @@ msgstr "" msgid "Test Report" msgstr "" -#: stock/templates/stock/location.html:13 +#: stock/templates/stock/location.html:18 msgid "All stock items" msgstr "" -#: stock/templates/stock/location.html:26 +#: stock/templates/stock/location.html:31 msgid "Check-in Items" msgstr "" -#: stock/templates/stock/location.html:37 +#: stock/templates/stock/location.html:42 msgid "Location actions" msgstr "" -#: stock/templates/stock/location.html:39 +#: stock/templates/stock/location.html:44 msgid "Edit location" msgstr "" -#: stock/templates/stock/location.html:40 +#: stock/templates/stock/location.html:45 msgid "Delete location" msgstr "" -#: stock/templates/stock/location.html:48 +#: stock/templates/stock/location.html:53 msgid "Location Details" msgstr "" -#: stock/templates/stock/location.html:53 +#: stock/templates/stock/location.html:58 msgid "Location Path" msgstr "" -#: stock/templates/stock/location.html:58 +#: stock/templates/stock/location.html:63 msgid "Location Description" msgstr "" -#: stock/templates/stock/location.html:63 +#: stock/templates/stock/location.html:68 msgid "Sublocations" msgstr "" -#: stock/templates/stock/location.html:68 -#: stock/templates/stock/location.html:83 +#: stock/templates/stock/location.html:73 +#: stock/templates/stock/location.html:88 #: templates/InvenTree/search_stock_items.html:6 templates/stats.html:21 #: templates/stats.html:30 msgid "Stock Items" msgstr "" -#: stock/templates/stock/location.html:73 +#: stock/templates/stock/location.html:78 msgid "Stock Details" msgstr "" -#: stock/templates/stock/location.html:78 +#: stock/templates/stock/location.html:83 #: templates/InvenTree/search_stock_location.html:6 templates/stats.html:25 msgid "Stock Locations" msgstr "" @@ -3524,7 +3576,7 @@ msgstr "" msgid "The following stock items will be uninstalled" msgstr "" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1186 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1287 msgid "Convert Stock Item" msgstr "" @@ -3636,130 +3688,134 @@ msgstr "" msgid "Stock Item QR Code" msgstr "" -#: stock/views.py:699 +#: stock/views.py:700 +msgid "Install Stock Item" +msgstr "" + +#: stock/views.py:799 msgid "Uninstall Stock Items" msgstr "" -#: stock/views.py:806 +#: stock/views.py:906 msgid "Uninstalled stock items" msgstr "" -#: stock/views.py:831 +#: stock/views.py:931 msgid "Adjust Stock" msgstr "" -#: stock/views.py:940 +#: stock/views.py:1040 msgid "Move Stock Items" msgstr "" -#: stock/views.py:941 +#: stock/views.py:1041 msgid "Count Stock Items" msgstr "" -#: stock/views.py:942 +#: stock/views.py:1042 msgid "Remove From Stock" msgstr "" -#: stock/views.py:943 +#: stock/views.py:1043 msgid "Add Stock Items" msgstr "" -#: stock/views.py:944 +#: stock/views.py:1044 msgid "Delete Stock Items" msgstr "" -#: stock/views.py:972 +#: stock/views.py:1072 msgid "Must enter integer value" msgstr "" -#: stock/views.py:977 +#: stock/views.py:1077 msgid "Quantity must be positive" msgstr "" -#: stock/views.py:984 +#: stock/views.py:1084 #, python-brace-format msgid "Quantity must not exceed {x}" msgstr "" -#: stock/views.py:1063 +#: stock/views.py:1163 #, python-brace-format msgid "Added stock to {n} items" msgstr "" -#: stock/views.py:1078 +#: stock/views.py:1178 #, python-brace-format msgid "Removed stock from {n} items" msgstr "" -#: stock/views.py:1091 +#: stock/views.py:1191 #, python-brace-format msgid "Counted stock for {n} items" msgstr "" -#: stock/views.py:1119 +#: stock/views.py:1219 msgid "No items were moved" msgstr "" -#: stock/views.py:1122 +#: stock/views.py:1222 #, python-brace-format msgid "Moved {n} items to {dest}" msgstr "" -#: stock/views.py:1141 +#: stock/views.py:1241 #, python-brace-format msgid "Deleted {n} stock items" msgstr "" -#: stock/views.py:1153 +#: stock/views.py:1253 msgid "Edit Stock Item" msgstr "" -#: stock/views.py:1234 +#: stock/views.py:1335 msgid "Serialize Stock" msgstr "" -#: stock/views.py:1426 +#: stock/views.py:1527 msgid "Duplicate Stock Item" msgstr "" -#: stock/views.py:1492 +#: stock/views.py:1593 msgid "Invalid quantity" msgstr "" -#: stock/views.py:1495 +#: stock/views.py:1596 msgid "Quantity cannot be less than zero" msgstr "" -#: stock/views.py:1499 +#: stock/views.py:1600 msgid "Invalid part selection" msgstr "" -#: stock/views.py:1548 +#: stock/views.py:1649 #, python-brace-format msgid "Created {n} new stock items" msgstr "" -#: stock/views.py:1567 stock/views.py:1583 +#: stock/views.py:1668 stock/views.py:1684 msgid "Created new stock item" msgstr "" -#: stock/views.py:1602 +#: stock/views.py:1703 msgid "Delete Stock Location" msgstr "" -#: stock/views.py:1615 +#: stock/views.py:1716 msgid "Delete Stock Item" msgstr "" -#: stock/views.py:1626 +#: stock/views.py:1727 msgid "Delete Stock Tracking Entry" msgstr "" -#: stock/views.py:1643 +#: stock/views.py:1744 msgid "Edit Stock Tracking Entry" msgstr "" -#: stock/views.py:1652 +#: stock/views.py:1753 msgid "Add Stock Tracking Entry" msgstr "" @@ -3787,15 +3843,19 @@ msgstr "" msgid "Search Results" msgstr "" -#: templates/InvenTree/search.html:22 -msgid "No results found" +#: templates/InvenTree/search.html:24 +msgid "No results found for " msgstr "" -#: templates/InvenTree/search.html:181 templates/js/stock.html:521 +#: templates/InvenTree/search.html:42 +msgid "Enter a search query" +msgstr "" + +#: templates/InvenTree/search.html:191 templates/js/stock.html:527 msgid "Shipped to customer" msgstr "" -#: templates/InvenTree/search.html:184 templates/js/stock.html:528 +#: templates/InvenTree/search.html:194 templates/js/stock.html:537 msgid "No stock location set" msgstr "" @@ -3974,31 +4034,35 @@ msgstr "" msgid "Open subassembly" msgstr "" -#: templates/js/bom.html:184 templates/js/build.html:119 +#: templates/js/bom.html:173 +msgid "Optional" +msgstr "" + +#: templates/js/bom.html:188 templates/js/build.html:119 msgid "Available" msgstr "" -#: templates/js/bom.html:209 +#: templates/js/bom.html:213 msgid "No pricing available" msgstr "" -#: templates/js/bom.html:228 +#: templates/js/bom.html:232 msgid "Actions" msgstr "" -#: templates/js/bom.html:236 +#: templates/js/bom.html:240 msgid "Validate BOM Item" msgstr "" -#: templates/js/bom.html:238 +#: templates/js/bom.html:242 msgid "This line has been validated" msgstr "" -#: templates/js/bom.html:240 +#: templates/js/bom.html:244 msgid "Edit BOM Item" msgstr "" -#: templates/js/bom.html:242 +#: templates/js/bom.html:246 msgid "Delete BOM Item" msgstr "" @@ -4026,11 +4090,11 @@ msgstr "" msgid "No supplier parts found" msgstr "" -#: templates/js/company.html:145 templates/js/part.html:248 +#: templates/js/company.html:145 templates/js/part.html:314 msgid "Template part" msgstr "" -#: templates/js/company.html:149 templates/js/part.html:252 +#: templates/js/company.html:149 templates/js/part.html:318 msgid "Assembled part" msgstr "" @@ -4042,7 +4106,7 @@ msgstr "" msgid "No purchase orders found" msgstr "" -#: templates/js/order.html:172 templates/js/stock.html:633 +#: templates/js/order.html:172 templates/js/stock.html:642 msgid "Date" msgstr "" @@ -4058,51 +4122,56 @@ msgstr "" msgid "No variants found" msgstr "" -#: templates/js/part.html:256 -msgid "Starred part" -msgstr "" - -#: templates/js/part.html:260 -msgid "Salable part" -msgstr "" - -#: templates/js/part.html:299 -msgid "No category" -msgstr "" - -#: templates/js/part.html:317 templates/js/table_filters.html:191 -msgid "Low stock" -msgstr "" - -#: templates/js/part.html:326 -msgid "Building" -msgstr "" - -#: templates/js/part.html:345 +#: templates/js/part.html:223 templates/js/part.html:411 msgid "No parts found" msgstr "" -#: templates/js/part.html:405 +#: templates/js/part.html:275 templates/js/stock.html:409 +#: templates/js/stock.html:965 +msgid "Select" +msgstr "" + +#: templates/js/part.html:322 +msgid "Starred part" +msgstr "" + +#: templates/js/part.html:326 +msgid "Salable part" +msgstr "" + +#: templates/js/part.html:365 +msgid "No category" +msgstr "" + +#: templates/js/part.html:383 templates/js/table_filters.html:196 +msgid "Low stock" +msgstr "" + +#: templates/js/part.html:392 +msgid "Building" +msgstr "" + +#: templates/js/part.html:471 msgid "YES" msgstr "" -#: templates/js/part.html:407 +#: templates/js/part.html:473 msgid "NO" msgstr "" -#: templates/js/part.html:441 +#: templates/js/part.html:507 msgid "No test templates matching query" msgstr "" -#: templates/js/part.html:492 templates/js/stock.html:63 +#: templates/js/part.html:558 templates/js/stock.html:63 msgid "Edit test result" msgstr "" -#: templates/js/part.html:493 templates/js/stock.html:64 +#: templates/js/part.html:559 templates/js/stock.html:64 msgid "Delete test result" msgstr "" -#: templates/js/part.html:499 +#: templates/js/part.html:565 msgid "This test is defined for a parent part" msgstr "" @@ -4146,42 +4215,62 @@ msgstr "" msgid "Stock item has been assigned to customer" msgstr "" -#: templates/js/stock.html:474 +#: templates/js/stock.html:475 msgid "Stock item was assigned to a build order" msgstr "" -#: templates/js/stock.html:476 +#: templates/js/stock.html:477 msgid "Stock item was assigned to a sales order" msgstr "" -#: templates/js/stock.html:483 +#: templates/js/stock.html:482 +msgid "Stock item has been installed in another item" +msgstr "" + +#: templates/js/stock.html:489 msgid "Stock item has been rejected" msgstr "" -#: templates/js/stock.html:487 +#: templates/js/stock.html:493 msgid "Stock item is lost" msgstr "" -#: templates/js/stock.html:491 templates/js/table_filters.html:60 +#: templates/js/stock.html:497 templates/js/table_filters.html:60 msgid "Depleted" msgstr "" -#: templates/js/stock.html:516 +#: templates/js/stock.html:522 msgid "Installed in Stock Item " msgstr "" -#: templates/js/stock.html:699 +#: templates/js/stock.html:530 +msgid "Assigned to sales order" +msgstr "" + +#: templates/js/stock.html:708 msgid "No user information" msgstr "" -#: templates/js/stock.html:783 +#: templates/js/stock.html:792 msgid "Create New Part" msgstr "" -#: templates/js/stock.html:795 +#: templates/js/stock.html:804 msgid "Create New Location" msgstr "" +#: templates/js/stock.html:903 +msgid "Serial" +msgstr "" + +#: templates/js/stock.html:996 templates/js/table_filters.html:70 +msgid "Installed" +msgstr "" + +#: templates/js/stock.html:1021 +msgid "Install item" +msgstr "" + #: templates/js/table_filters.html:19 templates/js/table_filters.html:80 msgid "Is Serialized" msgstr "" @@ -4243,10 +4332,6 @@ msgstr "" msgid "Show items which are in stock" msgstr "" -#: templates/js/table_filters.html:70 -msgid "Installed" -msgstr "" - #: templates/js/table_filters.html:71 msgid "Show stock items which are installed in another item" msgstr "" @@ -4283,19 +4368,27 @@ msgstr "" msgid "Include parts in subcategories" msgstr "" +#: templates/js/table_filters.html:178 +msgid "Has IPN" +msgstr "" + #: templates/js/table_filters.html:179 +msgid "Part has internal part number" +msgstr "" + +#: templates/js/table_filters.html:184 msgid "Show active parts" msgstr "" -#: templates/js/table_filters.html:187 +#: templates/js/table_filters.html:192 msgid "Stock available" msgstr "" -#: templates/js/table_filters.html:203 +#: templates/js/table_filters.html:208 msgid "Starred" msgstr "" -#: templates/js/table_filters.html:215 +#: templates/js/table_filters.html:220 msgid "Purchasable" msgstr "" @@ -4339,42 +4432,42 @@ msgstr "" msgid "Search" msgstr "" -#: templates/stock_table.html:5 +#: templates/stock_table.html:6 msgid "Export Stock Information" msgstr "" -#: templates/stock_table.html:12 +#: templates/stock_table.html:13 msgid "Add to selected stock items" msgstr "" -#: templates/stock_table.html:13 +#: templates/stock_table.html:14 msgid "Remove from selected stock items" msgstr "" -#: templates/stock_table.html:14 +#: templates/stock_table.html:15 msgid "Stocktake selected stock items" msgstr "" -#: templates/stock_table.html:15 +#: templates/stock_table.html:16 msgid "Move selected stock items" msgstr "" -#: templates/stock_table.html:15 +#: templates/stock_table.html:16 msgid "Move stock" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:17 msgid "Order selected items" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:17 msgid "Order stock" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:18 msgid "Delete selected items" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:18 msgid "Delete Stock" msgstr "" From 095ef51991b1f857bf52d5b3f2cff11a51164426 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 08:29:36 +1100 Subject: [PATCH 48/77] Cleanup unit testing --- InvenTree/users/tests.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index ad3a6ec5ae..d87649e2db 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -59,8 +59,6 @@ class RuleSetModelTest(TestCase): table_name = model.objects.model._meta.db_table available_tables.append(table_name) - errors = 0 - assigned_models = [] # Now check that each defined model is a valid table name @@ -72,10 +70,6 @@ class RuleSetModelTest(TestCase): assigned_models.append(m) - if m not in available_tables: - print("{n} is not a valid database table".format(n=m)) - errors += 1 - missing_models = [] for model in available_tables: @@ -87,5 +81,18 @@ class RuleSetModelTest(TestCase): for m in missing_models: print("-", m) - self.assertEqual(errors, 0) + extra_models = [] + + defined_models = assigned_models + RuleSet.RULESET_IGNORE + + for model in defined_models: + if model not in available_tables: + extra_models.append(model) + + if len(extra_models) > 0: + print("The following RuleSet permissions do not match a database model:") + for m in extra_models: + print("-", m) + self.assertEqual(len(missing_models), 0) + self.assertEqual(len(extra_models), 0) From 898c604b3b0d01094b715acadae76e35bdd15b65 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 08:55:15 +1100 Subject: [PATCH 49/77] Fix incorrect permission names - Uses the app_model name, *NOT* the name of the database table - Adds extra tests to ensure that permissions get assigned and removed correctly --- InvenTree/users/models.py | 19 ++++++---- InvenTree/users/tests.py | 79 ++++++++++++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 8f349e2f18..09f2a046d1 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -72,8 +72,8 @@ class RuleSet(models.Model): ], 'purchase_order': [ 'company_company', - 'part_supplierpart', - 'part_supplierpricebreak', + 'company_supplierpart', + 'company_supplierpricebreak', 'order_purchaseorder', 'order_purchaseorderattachment', 'order_purchaseorderlineitem', @@ -90,9 +90,9 @@ class RuleSet(models.Model): # Database models we ignore permission sets for RULESET_IGNORE = [ # Core django models (not user configurable) - 'django_admin_log', - 'django_content_type', - 'django_session', + 'admin_logentry', + 'contenttypes_contenttype', + 'sessions_session', # Models which currently do not require permissions 'common_colortheme', @@ -275,9 +275,12 @@ def update_group_roles(group, debug=False): (permission_name, model) = perm.split('_') - content_type = ContentType.objects.get(app_label=app, model=model) - - permission = Permission.objects.get(content_type=content_type, codename=perm) + try: + content_type = ContentType.objects.get(app_label=app, model=model) + permission = Permission.objects.get(content_type=content_type, codename=perm) + except ContentType.DoesNotExist: + print(f"Error: Could not find permission matching '{permission_string}'") + permission = None return permission diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index d87649e2db..d14ffc4950 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from django.test import TestCase from django.apps import apps +from django.contrib.auth.models import Group from users.models import RuleSet @@ -53,13 +54,15 @@ class RuleSetModelTest(TestCase): available_models = apps.get_models() - available_tables = [] + available_tables = set() + # Extract each available database model and construct a formatted string for model in available_models: - table_name = model.objects.model._meta.db_table - available_tables.append(table_name) + label = model.objects.model._meta.label + label = label.replace('.', '_').lower() + available_tables.add(label) - assigned_models = [] + assigned_models = set() # Now check that each defined model is a valid table name for key in RuleSet.RULESET_MODELS.keys(): @@ -68,26 +71,32 @@ class RuleSetModelTest(TestCase): for m in models: - assigned_models.append(m) + assigned_models.add(m) - missing_models = [] + missing_models = set() for model in available_tables: if model not in assigned_models and model not in RuleSet.RULESET_IGNORE: - missing_models.append(model) + missing_models.add(model) if len(missing_models) > 0: print("The following database models are not covered by the defined RuleSet permissions:") for m in missing_models: print("-", m) - extra_models = [] + extra_models = set() - defined_models = assigned_models + RuleSet.RULESET_IGNORE + defined_models = set() + + for model in assigned_models: + defined_models.add(model) + + for model in RuleSet.RULESET_IGNORE: + defined_models.add(model) for model in defined_models: if model not in available_tables: - extra_models.append(model) + extra_models.add(model) if len(extra_models) > 0: print("The following RuleSet permissions do not match a database model:") @@ -96,3 +105,53 @@ class RuleSetModelTest(TestCase): self.assertEqual(len(missing_models), 0) self.assertEqual(len(extra_models), 0) + + def test_permission_assign(self): + """ + Test that the permission assigning works! + """ + + # Create a new group + group = Group.objects.create(name="Test group") + + rulesets = group.rule_sets.all() + + # Rulesets should have been created automatically for this group + self.assertEqual(rulesets.count(), len(RuleSet.RULESET_CHOICES)) + + # Check that all permissions have been assigned permissions? + permission_set = set() + + for models in RuleSet.RULESET_MODELS.values(): + + for model in models: + permission_set.add(model) + + # Every ruleset by default sets one permission, the "view" permission set + self.assertEqual(group.permissions.count(), len(permission_set)) + + # Add some more rules + for rule in rulesets: + rule.can_add = True + rule.can_change = True + + rule.save() + + group.save() + + # There should now be three permissions for each rule set + self.assertEqual(group.permissions.count(), 3 * len(permission_set)) + + # Now remove *all* permissions + for rule in rulesets: + rule.can_view = False + rule.can_add = False + rule.can_change = False + rule.can_delete = False + + rule.save() + + group.save() + + # There should now not be any permissions assigned to this group + self.assertEqual(group.permissions.count(), 0) From 806a7f961de42a906a4be49b727ba5ce85001f15 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 22:57:05 +1100 Subject: [PATCH 50/77] Fixes for role permissions - Fixed a strange interaction if multiple rulesets referred to the same models - Order of operations was incorrect. - Now is good? Yes! --- InvenTree/users/models.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 09f2a046d1..5fe86e15fa 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -160,6 +160,15 @@ class RuleSet(models.Model): def save(self, *args, **kwargs): + # It does not make sense to be able to change / create something, + # but not be able to view it! + + if self.can_add or self.can_change or self.can_delete: + self.can_view = True + + if self.can_add or self.can_delete: + self.can_change = True + super().save(*args, **kwargs) def get_models(self): @@ -227,16 +236,13 @@ def update_group_roles(group, debug=False): if permission_string in permissions_to_delete: permissions_to_delete.remove(permission_string) - if permission_string not in group_permissions: - permissions_to_add.add(permission_string) + permissions_to_add.add(permission_string) else: # A forbidden action will be ignored if we have already allowed it if permission_string not in permissions_to_add: - - if permission_string in group_permissions: - permissions_to_delete.add(permission_string) + permissions_to_delete.add(permission_string) # Get all the rulesets associated with this group for r in RuleSet.RULESET_CHOICES: @@ -287,6 +293,10 @@ def update_group_roles(group, debug=False): # Add any required permissions to the group for perm in permissions_to_add: + # Ignore if permission is already in the group + if perm in group_permissions: + continue + permission = get_permission_object(perm) group.permissions.add(permission) @@ -297,6 +307,10 @@ def update_group_roles(group, debug=False): # Remove any extra permissions from the group for perm in permissions_to_delete: + # Ignore if the permission is not already assigned + if perm not in group_permissions: + continue + permission = get_permission_object(perm) group.permissions.remove(permission) From 3e9c7cda21dd37352039ae8a701b5292f3e6140d Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 23:11:55 +1100 Subject: [PATCH 51/77] Change what elements the user can see on the index page, based on permissions! --- InvenTree/templates/InvenTree/index.html | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index b0c2d0ae02..a9e4ae92ea 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -9,18 +9,26 @@ InvenTree | Index <hr> <div class='col-sm-6'> + {% if perms.part.view_part %} {% include "InvenTree/latest_parts.html" with collapse_id="latest_parts" %} {% include "InvenTree/bom_invalid.html" with collapse_id="bom_invalid" %} - {% include "InvenTree/low_stock.html" with collapse_id="order" %} - {% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %} - + {% include "InvenTree/starred_parts.html" with collapse_id="starred" %} + {% endif %} + {% if perms.build.view_build %} + {% include "InvenTree/build_pending.html" with collapse_id="build_pending" %} + {% endif %} </div> <div class='col-sm-6'> - {% include "InvenTree/starred_parts.html" with collapse_id="starred" %} - {% include "InvenTree/build_pending.html" with collapse_id="build_pending" %} + {% if perms.stock.view_stockitem %} + {% include "InvenTree/low_stock.html" with collapse_id="order" %} {% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %} + {% endif %} + {% if perms.order.view_purchaseorder %} + {% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %} + {% endif %} + {% if perms.order.view_salesorder %} {% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %} - + {% endif %} </div> {% endblock %} From 4d49cb029f3873172e323635845f2b631d81f186 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 23:49:32 +1100 Subject: [PATCH 52/77] Change part views to require permissions Also adds custom 403 page --- InvenTree/part/views.py | 93 ++++++++++++++++++++++-- InvenTree/templates/403.html | 18 +++++ InvenTree/templates/InvenTree/index.html | 4 +- 3 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 InvenTree/templates/403.html diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index dc8d07f5cd..84628eff04 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -12,6 +12,7 @@ from django.shortcuts import HttpResponseRedirect from django.utils.translation import gettext_lazy as _ from django.urls import reverse, reverse_lazy from django.views.generic import DetailView, ListView, FormView, UpdateView +from django.contrib.auth.mixins import PermissionRequiredMixin from django.forms.models import model_to_dict from django.forms import HiddenInput, CheckboxInput from django.conf import settings @@ -42,12 +43,13 @@ from InvenTree.views import QRCodeView from InvenTree.helpers import DownloadFile, str2bool -class PartIndex(ListView): +class PartIndex(PermissionRequiredMixin, ListView): """ View for displaying list of Part objects """ model = Part template_name = 'part/category.html' context_object_name = 'parts' + permission_required = ('part.view_part', 'part.view_partcategory') def get_queryset(self): return Part.objects.all().select_related('category') @@ -76,6 +78,8 @@ class PartAttachmentCreate(AjaxCreateView): ajax_form_title = _("Add part attachment") ajax_template_name = "modal_form.html" + permission_required = 'part.create_partattachment' + def post_save(self): """ Record the user that uploaded the attachment """ self.object.user = self.request.user @@ -123,6 +127,8 @@ class PartAttachmentEdit(AjaxUpdateView): form_class = part_forms.EditPartAttachmentForm ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit attachment') + + permission_required = 'part.change_partattachment' def get_data(self): return { @@ -145,6 +151,8 @@ class PartAttachmentDelete(AjaxDeleteView): ajax_template_name = "attachment_delete.html" context_object_name = "attachment" + permission_required = 'part.delete_partattachment' + def get_data(self): return { 'danger': _('Deleted part attachment') @@ -157,6 +165,8 @@ class PartTestTemplateCreate(AjaxCreateView): model = PartTestTemplate form_class = part_forms.EditPartTestTemplateForm ajax_form_title = _("Create Test Template") + + permission_required = 'part.create_parttesttemplate' def get_initial(self): @@ -185,6 +195,8 @@ class PartTestTemplateEdit(AjaxUpdateView): form_class = part_forms.EditPartTestTemplateForm ajax_form_title = _("Edit Test Template") + permission_required = 'part.change_parttesttemplate' + def get_form(self): form = super().get_form() @@ -199,6 +211,8 @@ class PartTestTemplateDelete(AjaxDeleteView): model = PartTestTemplate ajax_form_title = _("Delete Test Template") + permission_required = 'part.delete_parttesttemplate' + class PartSetCategory(AjaxUpdateView): """ View for settings the part category for multiple parts at once """ @@ -207,6 +221,8 @@ class PartSetCategory(AjaxUpdateView): ajax_form_title = _('Set Part Category') form_class = part_forms.SetPartCategoryForm + permission_required = 'part.change_part' + category = None parts = [] @@ -290,6 +306,8 @@ class MakePartVariant(AjaxCreateView): ajax_form_title = _('Create Variant') ajax_template_name = 'part/variant_part.html' + permission_required = 'part.create_part' + def get_part_template(self): return get_object_or_404(Part, id=self.kwargs['pk']) @@ -368,6 +386,8 @@ class PartDuplicate(AjaxCreateView): ajax_form_title = _("Duplicate Part") ajax_template_name = "part/copy_part.html" + permission_required = 'part.create_part' + def get_data(self): return { 'success': _('Copied part') @@ -491,6 +511,8 @@ class PartCreate(AjaxCreateView): ajax_form_title = _('Create new part') ajax_template_name = 'part/create_part.html' + permission_required = 'part.create_part' + def get_data(self): return { 'success': _("Created new part"), @@ -613,6 +635,8 @@ class PartNotes(UpdateView): template_name = 'part/notes.html' model = Part + permission_required = 'part.update_part' + fields = ['notes'] def get_success_url(self): @@ -634,7 +658,7 @@ class PartNotes(UpdateView): return ctx -class PartDetail(DetailView): +class PartDetail(PermissionRequiredMixin, DetailView): """ Detail view for Part object """ @@ -642,6 +666,8 @@ class PartDetail(DetailView): queryset = Part.objects.all().select_related('category') template_name = 'part/detail.html' + permission_required = 'part.view_part' + # Add in some extra context information based on query params def get_context_data(self, **kwargs): """ Provide extra context data to template @@ -706,6 +732,8 @@ class PartQRCode(QRCodeView): ajax_form_title = _("Part QR Code") + permission_required = 'part.view_part' + def get_qr_data(self): """ Generate QR code data for the Part """ @@ -722,8 +750,11 @@ class PartImageUpload(AjaxUpdateView): model = Part ajax_template_name = 'modal_form.html' ajax_form_title = _('Upload Part Image') + form_class = part_forms.PartImageForm + permission_required = 'part.update_part' + def get_data(self): return { 'success': _('Updated part image'), @@ -737,6 +768,8 @@ class PartImageSelect(AjaxUpdateView): ajax_template_name = 'part/select_image.html' ajax_form_title = _('Select Part Image') + permission_required = 'part.update_part' + fields = [ 'image', ] @@ -778,6 +811,8 @@ class PartEdit(AjaxUpdateView): ajax_form_title = _('Edit Part Properties') context_object_name = 'part' + permission_required = 'part.update_part' + def get_form(self): """ Create form for Part editing. Overrides default get_form() method to limit the choices @@ -802,6 +837,8 @@ class BomValidate(AjaxUpdateView): context_object_name = 'part' form_class = part_forms.BomValidateForm + permission_required = ('part.update_part') + def get_context(self): return { 'part': self.get_object(), @@ -832,7 +869,7 @@ class BomValidate(AjaxUpdateView): return self.renderJsonResponse(request, form, data, context=self.get_context()) -class BomUpload(FormView): +class BomUpload(PermissionRequiredMixin, FormView): """ View for uploading a BOM file, and handling BOM data importing. The BOM upload process is as follows: @@ -868,6 +905,8 @@ class BomUpload(FormView): missing_columns = [] allowed_parts = [] + permission_required = ('part.update_part', 'part.create_bomitem') + def get_success_url(self): part = self.get_object() return reverse('upload-bom', kwargs={'pk': part.id}) @@ -1466,6 +1505,8 @@ class BomUpload(FormView): class PartExport(AjaxView): """ Export a CSV file containing information on multiple parts """ + permission_required = 'part.view_part' + def get_parts(self, request): """ Extract part list from the POST parameters. Parts can be supplied as: @@ -1543,6 +1584,8 @@ class BomDownload(AjaxView): - File format should be passed as a query param e.g. ?format=csv """ + permission_required = ('part.view_part', 'part.view_bomitem') + model = Part def get(self, request, *args, **kwargs): @@ -1596,6 +1639,8 @@ class BomExport(AjaxView): form_class = part_forms.BomExportForm ajax_form_title = _("Export Bill of Materials") + permission_required = ('part.view_part', 'part.view_bomitem') + def get(self, request, *args, **kwargs): return self.renderJsonResponse(request, self.form_class()) @@ -1645,6 +1690,8 @@ class PartDelete(AjaxDeleteView): ajax_form_title = _('Confirm Part Deletion') context_object_name = 'part' + permission_required = 'part.delete_part' + success_url = '/part/' def get_data(self): @@ -1661,6 +1708,8 @@ class PartPricing(AjaxView): ajax_form_title = _("Part Pricing") form_class = part_forms.PartPriceForm + permission_required = ('company.view_supplierpricebreak', 'part.view_part') + def get_part(self): try: return Part.objects.get(id=self.kwargs['pk']) @@ -1778,6 +1827,8 @@ class PartPricing(AjaxView): class PartParameterTemplateCreate(AjaxCreateView): """ View for creating a new PartParameterTemplate """ + permission_required = 'part.create_partparametertemplate' + model = PartParameterTemplate form_class = part_forms.EditPartParameterTemplateForm ajax_form_title = _('Create Part Parameter Template') @@ -1786,6 +1837,8 @@ class PartParameterTemplateCreate(AjaxCreateView): class PartParameterTemplateEdit(AjaxUpdateView): """ View for editing a PartParameterTemplate """ + permission_required = 'part.change_partparametertemplate' + model = PartParameterTemplate form_class = part_forms.EditPartParameterTemplateForm ajax_form_title = _('Edit Part Parameter Template') @@ -1794,6 +1847,8 @@ class PartParameterTemplateEdit(AjaxUpdateView): class PartParameterTemplateDelete(AjaxDeleteView): """ View for deleting an existing PartParameterTemplate """ + permission_required = 'part.delete_partparametertemplate' + model = PartParameterTemplate ajax_form_title = _("Delete Part Parameter Template") @@ -1801,6 +1856,8 @@ class PartParameterTemplateDelete(AjaxDeleteView): class PartParameterCreate(AjaxCreateView): """ View for creating a new PartParameter """ + permission_required = 'part.create_partparameter' + model = PartParameter form_class = part_forms.EditPartParameterForm ajax_form_title = _('Create Part Parameter') @@ -1851,6 +1908,8 @@ class PartParameterCreate(AjaxCreateView): class PartParameterEdit(AjaxUpdateView): """ View for editing a PartParameter """ + permission_required = 'part.change_partparameter' + model = PartParameter form_class = part_forms.EditPartParameterForm ajax_form_title = _('Edit Part Parameter') @@ -1865,12 +1924,14 @@ class PartParameterEdit(AjaxUpdateView): class PartParameterDelete(AjaxDeleteView): """ View for deleting a PartParameter """ + permission_required = 'part.delete_partparameter' + model = PartParameter ajax_template_name = 'part/param_delete.html' ajax_form_title = _('Delete Part Parameter') -class CategoryDetail(DetailView): +class CategoryDetail(PermissionRequiredMixin, DetailView): """ Detail view for PartCategory """ model = PartCategory @@ -1878,6 +1939,8 @@ class CategoryDetail(DetailView): queryset = PartCategory.objects.all().prefetch_related('children') template_name = 'part/category_partlist.html' + permission_required = 'part.view_partcategory' + def get_context_data(self, **kwargs): context = super(CategoryDetail, self).get_context_data(**kwargs).copy() @@ -1926,6 +1989,8 @@ class CategoryEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit Part Category') + permission_required = 'part.change_partcategory' + def get_context_data(self, **kwargs): context = super(CategoryEdit, self).get_context_data(**kwargs).copy() @@ -1963,6 +2028,8 @@ class CategoryDelete(AjaxDeleteView): context_object_name = 'category' success_url = '/part/' + permission_required = 'part.delete_partcategory' + def get_data(self): return { 'danger': _('Part category was deleted'), @@ -1977,6 +2044,8 @@ class CategoryCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' form_class = part_forms.EditCategoryForm + permission_required = 'part.create_partcategory' + def get_context_data(self, **kwargs): """ Add extra context data to template. @@ -2012,12 +2081,14 @@ class CategoryCreate(AjaxCreateView): return initials -class BomItemDetail(DetailView): +class BomItemDetail(PermissionRequiredMixin, DetailView): """ Detail view for BomItem """ context_object_name = 'item' queryset = BomItem.objects.all() template_name = 'part/bom-detail.html' + permission_required = 'part.view_bomitem' + class BomItemCreate(AjaxCreateView): """ Create view for making a new BomItem object """ @@ -2026,6 +2097,8 @@ class BomItemCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Create BOM item') + permission_required = 'part.create_bomitem' + def get_form(self): """ Override get_form() method to reduce Part selection options. @@ -2092,6 +2165,8 @@ class BomItemEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit BOM item') + permission_required = 'part.change_bomitem' + def get_form(self): """ Override get_form() method to filter part selection options @@ -2140,6 +2215,8 @@ class BomItemDelete(AjaxDeleteView): context_object_name = 'item' ajax_form_title = _('Confim BOM item deletion') + permission_required = 'part.delete_bomitem' + class PartSalePriceBreakCreate(AjaxCreateView): """ View for creating a sale price break for a part """ @@ -2147,6 +2224,8 @@ class PartSalePriceBreakCreate(AjaxCreateView): model = PartSellPriceBreak form_class = part_forms.EditPartSalePriceBreakForm ajax_form_title = _('Add Price Break') + + permission_required = 'part.create_partsellpricebreak' def get_data(self): return { @@ -2197,6 +2276,8 @@ class PartSalePriceBreakEdit(AjaxUpdateView): form_class = part_forms.EditPartSalePriceBreakForm ajax_form_title = _('Edit Price Break') + permission_required = 'part.change_partsellpricebreak' + def get_form(self): form = super().get_form() @@ -2211,3 +2292,5 @@ class PartSalePriceBreakDelete(AjaxDeleteView): model = PartSellPriceBreak ajax_form_title = _("Delete Price Break") ajax_template_name = "modal_delete_form.html" + + permission_required = 'part.delete_partsalepricebreak' diff --git a/InvenTree/templates/403.html b/InvenTree/templates/403.html new file mode 100644 index 0000000000..372bd9fe27 --- /dev/null +++ b/InvenTree/templates/403.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block page_title %} +InvenTree | {% trans "Permission Denied" %} +{% endblock %} + +{% block content %} + +<div class='container-fluid'> + <h3>{% trans "Permission Denied" %}</h3> + + <div class='alert alert-danger alert-block'> + {% trans "You do not have permission to view this page." %} + </div> +</div> + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index a9e4ae92ea..b20d61116d 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -1,7 +1,7 @@ {% extends "base.html" %} - +{% load i18n %} {% block page_title %} -InvenTree | Index +InvenTree | {% trans "Index" %} {% endblock %} {% block content %} From 66bdce3d04d1a08be9b87e7016ce6100b8dbf4b2 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Mon, 5 Oct 2020 23:53:24 +1100 Subject: [PATCH 53/77] Hide elements on the PartCategory page, based on permissions --- InvenTree/part/templates/part/category.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 9a6fb9d7e5..44c8cd7df9 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -20,17 +20,23 @@ {% endif %} <p> <div class='btn-group action-buttons'> + {% if perms.part.create_partcategory %} <button class='btn btn-default' id='cat-create' title='{% trans "Create new part category" %}'> <span class='fas fa-plus-circle icon-green'/> </button> + {% endif %} {% if category %} + {% if perms.part.change_partcategory %} <button class='btn btn-default' id='cat-edit' title='{% trans "Edit part category" %}'> <span class='fas fa-edit icon-blue'/> </button> + {% endif %} + {% if perms.part.delete_partcategory %} <button class='btn btn-default' id='cat-delete' title='{% trans "Delete part category" %}'> <span class='fas fa-trash-alt icon-red'/> </button> {% endif %} + {% endif %} </div> </p> </div> @@ -104,11 +110,15 @@ <div class='button-toolbar container-fluid' style="float: right;"> <div class='btn-group'> <button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>{% trans "Export" %}</button> + {% if perms.part.create_part %} <button class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>{% trans "New Part" %}</button> + {% endif %} <div class='btn-group'> <button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown">{% trans "Options" %}<span class='caret'></span></button> <ul class='dropdown-menu'> + {% if perms.part.change_part %} <li><a href='#' id='multi-part-category' title='{% trans "Set category" %}'>{% trans "Set Category" %}</a></li> + {% endif %} <li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li> <li><a href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li> </ul> @@ -180,6 +190,7 @@ location.href = url; }); + {% if perms.part.create_part %} $("#part-create").click(function() { launchModalForm( "{% url 'part-create' %}", @@ -207,6 +218,7 @@ } ); }); + {% endif %} {% if category %} $("#cat-edit").click(function () { From afadd51a1437e15cad23ff51a45bb0cb76c7bb41 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 00:19:44 +1100 Subject: [PATCH 54/77] Fix permissions in views.py Silly, "add" not "create" --- InvenTree/part/views.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 84628eff04..47a77078be 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -78,7 +78,7 @@ class PartAttachmentCreate(AjaxCreateView): ajax_form_title = _("Add part attachment") ajax_template_name = "modal_form.html" - permission_required = 'part.create_partattachment' + permission_required = 'part.add_partattachment' def post_save(self): """ Record the user that uploaded the attachment """ @@ -166,7 +166,7 @@ class PartTestTemplateCreate(AjaxCreateView): form_class = part_forms.EditPartTestTemplateForm ajax_form_title = _("Create Test Template") - permission_required = 'part.create_parttesttemplate' + permission_required = 'part.add_parttesttemplate' def get_initial(self): @@ -306,7 +306,7 @@ class MakePartVariant(AjaxCreateView): ajax_form_title = _('Create Variant') ajax_template_name = 'part/variant_part.html' - permission_required = 'part.create_part' + permission_required = 'part.add_part' def get_part_template(self): return get_object_or_404(Part, id=self.kwargs['pk']) @@ -386,7 +386,7 @@ class PartDuplicate(AjaxCreateView): ajax_form_title = _("Duplicate Part") ajax_template_name = "part/copy_part.html" - permission_required = 'part.create_part' + permission_required = 'part.add_part' def get_data(self): return { @@ -511,7 +511,7 @@ class PartCreate(AjaxCreateView): ajax_form_title = _('Create new part') ajax_template_name = 'part/create_part.html' - permission_required = 'part.create_part' + permission_required = 'part.add_part' def get_data(self): return { @@ -905,7 +905,7 @@ class BomUpload(PermissionRequiredMixin, FormView): missing_columns = [] allowed_parts = [] - permission_required = ('part.update_part', 'part.create_bomitem') + permission_required = ('part.update_part', 'part.add_bomitem') def get_success_url(self): part = self.get_object() @@ -1827,7 +1827,7 @@ class PartPricing(AjaxView): class PartParameterTemplateCreate(AjaxCreateView): """ View for creating a new PartParameterTemplate """ - permission_required = 'part.create_partparametertemplate' + permission_required = 'part.add_partparametertemplate' model = PartParameterTemplate form_class = part_forms.EditPartParameterTemplateForm @@ -1856,7 +1856,7 @@ class PartParameterTemplateDelete(AjaxDeleteView): class PartParameterCreate(AjaxCreateView): """ View for creating a new PartParameter """ - permission_required = 'part.create_partparameter' + permission_required = 'part.add_partparameter' model = PartParameter form_class = part_forms.EditPartParameterForm @@ -2044,7 +2044,7 @@ class CategoryCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' form_class = part_forms.EditCategoryForm - permission_required = 'part.create_partcategory' + permission_required = 'part.add_partcategory' def get_context_data(self, **kwargs): """ Add extra context data to template. @@ -2097,7 +2097,7 @@ class BomItemCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Create BOM item') - permission_required = 'part.create_bomitem' + permission_required = 'part.add_bomitem' def get_form(self): """ Override get_form() method to reduce Part selection options. @@ -2225,7 +2225,7 @@ class PartSalePriceBreakCreate(AjaxCreateView): form_class = part_forms.EditPartSalePriceBreakForm ajax_form_title = _('Add Price Break') - permission_required = 'part.create_partsellpricebreak' + permission_required = 'part.add_partsellpricebreak' def get_data(self): return { From ba4c829b10137ca1a37cf7d1dde0b73ce8d0cb57 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 00:20:45 +1100 Subject: [PATCH 55/77] Add permission requirements in various part templates --- InvenTree/part/templates/part/bom.html | 2 ++ InvenTree/part/templates/part/build.html | 7 +++++-- InvenTree/part/templates/part/category.html | 6 +++--- InvenTree/part/templates/part/notes.html | 2 ++ InvenTree/part/templates/part/params.html | 8 +++++++ InvenTree/part/templates/part/part_base.html | 22 ++++++++++++++++---- InvenTree/part/templates/part/tabs.html | 9 +++++--- InvenTree/templates/slide.html | 2 +- InvenTree/templates/stock_table.html | 8 +++++++ 9 files changed, 53 insertions(+), 13 deletions(-) diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 2653a42575..610e60847a 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -39,10 +39,12 @@ <button class='btn btn-default action-button' type='button' title='{% trans "New BOM Item" %}' id='bom-item-new'><span class='fas fa-plus-circle'></span></button> <button class='btn btn-default action-button' type='button' title='{% trans "Finish Editing" %}' id='editing-finished'><span class='fas fa-check-circle'></span></button> {% elif part.active %} + {% if perms.part.change_part %} <button class='btn btn-default action-button' type='button' title='{% trans "Edit BOM" %}' id='edit-bom'><span class='fas fa-edit'></span></button> {% if part.is_bom_valid == False %} <button class='btn btn-default action-button' id='validate-bom' title='{% trans "Validate Bill of Materials" %}' type='button'><span class='fas fa-clipboard-check'></span></button> {% endif %} + {% endif %} <button title='{% trans "Export Bill of Materials" %}' class='btn btn-default action-button' id='download-bom' type='button'><span class='fas fa-file-download'></span></button> {% endif %} </div> diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index 442693ddeb..ad51d33ab2 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -1,15 +1,18 @@ {% extends "part/part_base.html" %} {% load static %} +{% load i18n %} {% block details %} {% include 'part/tabs.html' with tab='build' %} -<h3>Part Builds</h3> +<h3>{% trans "Part Builds" %}</h3> <div id='button-toolbar'> <div class='button-toolbar container-flui' style='float: right';> {% if part.active %} - <button class="btn btn-success" id='start-build'>Start New Build</button> + {% if perms.build.add_build %} + <button class="btn btn-success" id='start-build'>{% trans "Start New Build" %}</button> + {% endif %} {% endif %} <div class='filter-list' id='filter-list-build'> <!-- Empty div for filters --> diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 44c8cd7df9..25417a1d9b 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -20,7 +20,7 @@ {% endif %} <p> <div class='btn-group action-buttons'> - {% if perms.part.create_partcategory %} + {% if perms.part.add_partcategory %} <button class='btn btn-default' id='cat-create' title='{% trans "Create new part category" %}'> <span class='fas fa-plus-circle icon-green'/> </button> @@ -110,7 +110,7 @@ <div class='button-toolbar container-fluid' style="float: right;"> <div class='btn-group'> <button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>{% trans "Export" %}</button> - {% if perms.part.create_part %} + {% if perms.part.add_part %} <button class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>{% trans "New Part" %}</button> {% endif %} <div class='btn-group'> @@ -190,7 +190,7 @@ location.href = url; }); - {% if perms.part.create_part %} + {% if perms.part.add_part %} $("#part-create").click(function() { launchModalForm( "{% url 'part-create' %}", diff --git a/InvenTree/part/templates/part/notes.html b/InvenTree/part/templates/part/notes.html index afa215600d..68711c881e 100644 --- a/InvenTree/part/templates/part/notes.html +++ b/InvenTree/part/templates/part/notes.html @@ -29,7 +29,9 @@ <h4>{% trans "Part Notes" %}</h4> </div> <div class='col-sm-6'> + {% if perms.part.change_part %} <button title='{% trans "Edit notes" %}' class='btn btn-default action-button float-right' id='edit-notes'><span class='fas fa-edit'></span></button> + {% endif %} </div> </div> <hr> diff --git a/InvenTree/part/templates/part/params.html b/InvenTree/part/templates/part/params.html index 8ad4f61252..9984830242 100644 --- a/InvenTree/part/templates/part/params.html +++ b/InvenTree/part/templates/part/params.html @@ -10,7 +10,9 @@ <div id='button-toolbar'> <div class='button-toolbar container-fluid' style='float: right;'> + {% if perms.part.add_partparameter %} <button title='{% trans "Add new parameter" %}' class='btn btn-success' id='param-create'>{% trans "New Parameter" %}</button> + {% endif %} </div> </div> @@ -30,8 +32,12 @@ <td> {{ param.template.units }} <div class='btn-group' style='float: right;'> + {% if perms.part.change_partparameter %} <button title='{% trans "Edit" %}' class='btn btn-default btn-glyph param-edit' url="{% url 'part-param-edit' param.id %}" type='button'><span class='fas fa-edit'/></button> + {% endif %} + {% if perms.part.delete_partparameter %} <button title='{% trans "Delete" %}' class='btn btn-default btn-glyph param-delete' url="{% url 'part-param-delete' param.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button> + {% endif %} </div> </td> </tr> @@ -48,6 +54,7 @@ $('#param-table').inventreeTable({ }); + {% if perms.part.add_partparameter %} $('#param-create').click(function() { launchModalForm("{% url 'part-param-create' %}?part={{ part.id }}", { reload: true, @@ -59,6 +66,7 @@ }], }); }); + {% endif %} $('.param-edit').click(function() { var button = $(this); diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 1de4976740..6103cb5369 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -56,26 +56,36 @@ <button type='button' class='btn btn-default' id='price-button' title='{% trans "Show pricing information" %}'> <span id='part-price-icon' class='fas fa-dollar-sign'/> </button> - <button type='button' class='btn btn-default' id='part-count' title='Count part stock'> + {% if perms.stock.change_stockitem %} + <button type='button' class='btn btn-default' id='part-count' title='{% trans "Count part stock" %}'> <span class='fas fa-clipboard-list'/> </button> + {% endif %} {% if part.purchaseable %} - <button type='button' class='btn btn-default' id='part-order' title='Order part'> + {% if perms.order.add_purchaseorder %} + <button type='button' class='btn btn-default' id='part-order' title='{% trans "Order part" %}'> <span id='part-order-icon' class='fas fa-shopping-cart'/> </button> {% endif %} {% endif %} + {% endif %} <!-- Part actions --> + {% if perms.part.add_part or perms.part.change_part or perms.part.delete_part %} <div class='btn-group'> <button id='part-actions' title='{% trans "Part actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'> <span class='fas fa-shapes'></span> <span class='caret'></span></button> <ul class='dropdown-menu'> + {% if perms.part.add_part %} <li><a href='#' id='part-duplicate'><span class='fas fa-copy'></span> {% trans "Duplicate part" %}</a></li> + {% endif %} + {% if perms.part.change_part %} <li><a href='#' id='part-edit'><span class='fas fa-edit icon-blue'></span> {% trans "Edit part" %}</a></li> - {% if not part.active %} + {% endif %} + {% if not part.active and perms.part.delete_part %} <li><a href='#' id='part-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete part" %}</a></li> {% endif %} </ul> </div> + {% endif %} </div> <table class='table table-condensed'> <col width='25'> @@ -274,6 +284,7 @@ }); }); + {% if perms.part.change_part %} $("#part-edit").click(function() { launchModalForm( "{% url 'part-edit' part.id %}", @@ -282,6 +293,7 @@ } ); }); + {% endif %} $("#part-order").click(function() { launchModalForm("{% url 'order-parts' %}", { @@ -292,6 +304,7 @@ }); }); + {% if perms.part.add_part %} $("#part-duplicate").click(function() { launchModalForm( "{% url 'part-duplicate' part.id %}", @@ -300,8 +313,9 @@ } ); }); + {% endif %} - {% if not part.active %} + {% if not part.active and perms.part.delete_part %} $("#part-delete").click(function() { launchModalForm( "{% url 'part-delete' part.id %}", diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 1eab299ed5..02d8672979 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -26,14 +26,17 @@ {% if part.assembly %} <li{% ifequal tab 'bom' %} class="active"{% endifequal %}> <a href="{% url 'part-bom' part.id %}">{% trans "BOM" %}<span class="badge{% if part.is_bom_valid == False %} badge-alert{% endif %}">{{ part.bom_count }}</span></a></li> + {% if perms.build.view_build %} <li{% ifequal tab 'build' %} class="active"{% endifequal %}> - <a href="{% url 'part-build' part.id %}">{% trans "Build Orders" %}<span class='badge'>{{ part.builds.count }}</span></a></li> + <a href="{% url 'part-build' part.id %}">{% trans "Build Orders" %}<span class='badge'>{{ part.builds.count }}</span></a> + </li> + {% endif %} {% endif %} {% if part.component or part.used_in_count > 0 %} <li{% ifequal tab 'used' %} class="active"{% endifequal %}> <a href="{% url 'part-used-in' part.id %}">{% trans "Used In" %} {% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li> {% endif %} - {% if part.purchaseable %} + {% if part.purchaseable and perms.order.view_purchaseorder %} {% if part.is_template == False %} <li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}> <a href="{% url 'part-suppliers' part.id %}">{% trans "Suppliers" %} @@ -45,7 +48,7 @@ <a href="{% url 'part-orders' part.id %}">{% trans "Purchase Orders" %} <span class='badge'>{{ part.purchase_orders|length }}</span></a> </li> {% endif %} - {% if part.salable %} + {% if part.salable and perms.order.view_salesorder %} <li {% if tab == 'sales-prices' %}class='active'{% endif %}> <a href="{% url 'part-sale-prices' part.id %}">{% trans "Sale Price" %}</a> </li> diff --git a/InvenTree/templates/slide.html b/InvenTree/templates/slide.html index e2fe2e932f..45535786c9 100644 --- a/InvenTree/templates/slide.html +++ b/InvenTree/templates/slide.html @@ -1,3 +1,3 @@ <div> - <input fieldname='{{ field }}' class='slidey' type="checkbox" data-offstyle='warning' data-onstyle="success" data-size='small' data-toggle="toggle" {% if disabled %}disabled {% endif %}{% if state %}checked=""{% endif %} autocomplete="off"> + <input fieldname='{{ field }}' class='slidey' type="checkbox" data-offstyle='warning' data-onstyle="success" data-size='small' data-toggle="toggle" {% if disabled or not perms.part.change_part %}disabled {% endif %}{% if state %}checked=""{% endif %} autocomplete="off"> </div> \ No newline at end of file diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html index c41d2181c9..be26b1a976 100644 --- a/InvenTree/templates/stock_table.html +++ b/InvenTree/templates/stock_table.html @@ -6,19 +6,27 @@ <button class='btn btn-default' id='stock-export' title='{% trans "Export Stock Information" %}'>{% trans "Export" %}</button> {% if read_only %} {% else %} + {% if perms.stock.add_stockitem %} <button class="btn btn-success" id='item-create'>{% trans "New Stock Item" %}</button> + {% endif %} + {% if perms.stock.change_stockitem or perms.stock.delete_stockitem %} <div class="btn-group"> <button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button> <ul class="dropdown-menu"> + {% if perms.stock.change_stockitem %} <li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'>{% trans "Add stock" %}</a></li> <li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'>{% trans "Remove stock" %}</a></li> <li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'>{% trans "Count stock" %}</a></li> <li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'>{% trans "Move stock" %}</a></li> <li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'>{% trans "Order stock" %}</a></li> + {% endif %} + {% if perms.stock.delete_stockitem %} <li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'>{% trans "Delete Stock" %}</a></li> + {% endif %} </ul> </div> {% endif %} + {% endif %} </div> <div class='filter-list' id='filter-list-stock'> <!-- An empty div in which the filter list will be constructed --> From 8ee16d6f98600e4a618254f1dc0a79ab7113c2ed Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 00:20:57 +1100 Subject: [PATCH 56/77] update translation files --- InvenTree/locale/de/LC_MESSAGES/django.po | 405 ++++++++++++++-------- InvenTree/locale/en/LC_MESSAGES/django.po | 397 ++++++++++++--------- InvenTree/locale/es/LC_MESSAGES/django.po | 397 ++++++++++++--------- 3 files changed, 731 insertions(+), 468 deletions(-) diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po index 493d4a8b06..7632fadc4c 100644 --- a/InvenTree/locale/de/LC_MESSAGES/django.po +++ b/InvenTree/locale/de/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-04 14:02+0000\n" +"POT-Creation-Date: 2020-10-05 13:20+0000\n" "PO-Revision-Date: 2020-05-03 11:32+0200\n" "Last-Translator: Christian Schlüter <chschlue@gmail.com>\n" "Language-Team: C <kde-i18n-doc@kde.org>\n" @@ -96,7 +96,7 @@ msgstr "Datei-Kommentar" msgid "User" msgstr "Benutzer" -#: InvenTree/models.py:106 part/templates/part/params.html:20 +#: InvenTree/models.py:106 part/templates/part/params.html:22 #: templates/js/part.html:81 msgid "Name" msgstr "Name" @@ -107,19 +107,19 @@ msgstr "Name" msgid "Description (optional)" msgstr "Firmenbeschreibung" -#: InvenTree/settings.py:342 +#: InvenTree/settings.py:343 msgid "English" msgstr "Englisch" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:344 msgid "German" msgstr "Deutsch" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:345 msgid "French" msgstr "Französisch" -#: InvenTree/settings.py:345 +#: InvenTree/settings.py:346 msgid "Polish" msgstr "Polnisch" @@ -345,7 +345,7 @@ msgstr "Bau-Anzahl" msgid "Number of parts to build" msgstr "Anzahl der zu bauenden Teile" -#: build/models.py:128 part/templates/part/part_base.html:145 +#: build/models.py:128 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "Bau-Status" @@ -364,7 +364,7 @@ msgstr "Chargennummer für diese Bau-Ausgabe" #: build/models.py:155 build/templates/build/detail.html:55 #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 -#: part/templates/part/detail.html:80 part/templates/part/part_base.html:92 +#: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 #: stock/models.py:381 stock/templates/stock/item_base.html:244 msgid "External Link" msgstr "Externer Link" @@ -376,7 +376,7 @@ msgstr "Link zu einer externen URL" #: build/models.py:160 build/templates/build/tabs.html:14 company/models.py:310 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:15 #: order/templates/order/purchase_order_detail.html:202 -#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:67 +#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:70 #: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 #: stock/models.py:1404 stock/templates/stock/tabs.html:26 #: templates/js/barcode.html:391 templates/js/bom.html:223 @@ -425,7 +425,7 @@ msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" #: build/templates/build/allocate.html:17 #: company/templates/company/detail_part.html:18 order/views.py:779 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 msgid "Order Parts" msgstr "Teile bestellen" @@ -441,7 +441,7 @@ msgstr "Automatisches Zuweisen" msgid "Unallocate" msgstr "Zuweisung aufheben" -#: build/templates/build/allocate.html:87 templates/stock_table.html:9 +#: build/templates/build/allocate.html:87 templates/stock_table.html:10 msgid "New Stock Item" msgstr "Neues Lagerobjekt" @@ -574,7 +574,7 @@ msgstr "Lagerobjekt wurde zugewiesen" #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 #: stock/templates/stock/item_base.html:223 templates/js/build.html:39 -#: templates/navbar.html:20 +#: templates/navbar.html:25 msgid "Build" msgstr "Bau" @@ -717,7 +717,7 @@ msgstr "Fertig" #: build/templates/build/index.html:6 build/templates/build/index.html:14 #: order/templates/order/so_builds.html:11 order/templates/order/so_tabs.html:9 -#: part/templates/part/tabs.html:30 +#: part/templates/part/tabs.html:31 users/models.py:30 msgid "Build Orders" msgstr "Bauaufträge" @@ -739,7 +739,7 @@ msgstr "Speichern" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 #: order/templates/order/order_notes.html:32 #: order/templates/order/sales_order_notes.html:37 -#: part/templates/part/notes.html:32 stock/templates/stock/item_notes.html:33 +#: part/templates/part/notes.html:33 stock/templates/stock/item_notes.html:33 msgid "Edit notes" msgstr "Bermerkungen bearbeiten" @@ -1085,13 +1085,13 @@ msgid "New Supplier Part" msgstr "Neues Zulieferer-Teil" #: company/templates/company/detail_part.html:15 -#: part/templates/part/category.html:109 part/templates/part/supplier.html:15 -#: templates/stock_table.html:11 +#: part/templates/part/category.html:117 part/templates/part/supplier.html:15 +#: templates/stock_table.html:14 msgid "Options" msgstr "Optionen" #: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 #, fuzzy #| msgid "Order part" msgid "Order parts" @@ -1108,7 +1108,7 @@ msgid "Delete Parts" msgstr "Teile löschen" #: company/templates/company/detail_part.html:43 -#: part/templates/part/category.html:107 templates/js/stock.html:791 +#: part/templates/part/category.html:114 templates/js/stock.html:791 msgid "New Part" msgstr "Neues Teil" @@ -1140,7 +1140,7 @@ msgstr "Zuliefererbestand" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:106 part/templates/part/category.html:113 +#: part/templates/part/category.html:112 part/templates/part/category.html:123 #: part/templates/part/stock.html:51 templates/stock_table.html:6 msgid "Export" msgstr "Exportieren" @@ -1163,8 +1163,8 @@ msgstr "" #: company/templates/company/tabs.html:17 #: order/templates/order/purchase_orders.html:7 #: order/templates/order/purchase_orders.html:12 -#: part/templates/part/orders.html:9 part/templates/part/tabs.html:45 -#: templates/navbar.html:26 +#: part/templates/part/orders.html:9 part/templates/part/tabs.html:48 +#: templates/navbar.html:33 users/models.py:31 msgid "Purchase Orders" msgstr "Bestellungen" @@ -1182,8 +1182,8 @@ msgstr "Neue Bestellung" #: company/templates/company/tabs.html:22 #: order/templates/order/sales_orders.html:7 #: order/templates/order/sales_orders.html:12 -#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:53 -#: templates/navbar.html:33 +#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:56 +#: templates/navbar.html:42 users/models.py:32 msgid "Sales Orders" msgstr "Bestellungen" @@ -1204,7 +1204,7 @@ msgid "Supplier Part" msgstr "Zulieferer-Teil" #: company/templates/company/supplier_part_base.html:23 -#: part/templates/part/orders.html:14 +#: part/templates/part/orders.html:14 part/templates/part/part_base.html:66 msgid "Order part" msgstr "Teil bestellen" @@ -1251,7 +1251,7 @@ msgid "Pricing Information" msgstr "Preisinformationen ansehen" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2149 +#: part/templates/part/sale_prices.html:13 part/views.py:2226 msgid "Add Price Break" msgstr "Preisstaffel hinzufügen" @@ -1293,7 +1293,7 @@ msgstr "Bepreisung" #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 #: templates/js/part.html:124 templates/js/part.html:372 -#: templates/js/stock.html:452 templates/navbar.html:19 +#: templates/js/stock.html:452 templates/navbar.html:22 users/models.py:29 msgid "Stock" msgstr "Lagerbestand" @@ -1303,22 +1303,22 @@ msgstr "Bestellungen" #: company/templates/company/tabs.html:9 #: order/templates/order/receive_parts.html:14 part/models.py:294 -#: part/templates/part/cat_link.html:7 part/templates/part/category.html:88 -#: part/templates/part/category_tabs.html:6 templates/navbar.html:18 -#: templates/stats.html:8 templates/stats.html:17 +#: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 +#: part/templates/part/category_tabs.html:6 templates/navbar.html:19 +#: templates/stats.html:8 templates/stats.html:17 users/models.py:28 msgid "Parts" msgstr "Teile" -#: company/views.py:50 part/templates/part/tabs.html:39 -#: templates/navbar.html:24 +#: company/views.py:50 part/templates/part/tabs.html:42 +#: templates/navbar.html:31 msgid "Suppliers" msgstr "Zulieferer" -#: company/views.py:57 templates/navbar.html:25 +#: company/views.py:57 templates/navbar.html:32 msgid "Manufacturers" msgstr "Hersteller" -#: company/views.py:64 templates/navbar.html:32 +#: company/views.py:64 templates/navbar.html:41 msgid "Customers" msgstr "Kunden" @@ -1382,17 +1382,17 @@ msgstr "Neues Zuliefererteil anlegen" msgid "Delete Supplier Part" msgstr "Zuliefererteil entfernen" -#: company/views.py:404 part/views.py:2153 +#: company/views.py:404 part/views.py:2232 #, fuzzy #| msgid "Add Price Break" msgid "Added new price break" msgstr "Preisstaffel hinzufügen" -#: company/views.py:441 part/views.py:2198 +#: company/views.py:441 part/views.py:2277 msgid "Edit Price Break" msgstr "Preisstaffel bearbeiten" -#: company/views.py:456 part/views.py:2212 +#: company/views.py:456 part/views.py:2293 msgid "Delete Price Break" msgstr "Preisstaffel löschen" @@ -1497,7 +1497,7 @@ msgstr "" msgid "Date order was completed" msgstr "Bestellung als vollständig markieren" -#: order/models.py:185 order/models.py:259 part/views.py:1304 +#: order/models.py:185 order/models.py:259 part/views.py:1343 #: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "Anzahl muss größer Null sein" @@ -1667,7 +1667,7 @@ msgid "Purchase Order Attachments" msgstr "Bestellanhänge" #: order/templates/order/po_tabs.html:8 order/templates/order/so_tabs.html:16 -#: part/templates/part/tabs.html:64 stock/templates/stock/tabs.html:32 +#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 msgid "Attachments" msgstr "Anhänge" @@ -1683,7 +1683,7 @@ msgstr "Bestellpositionen" #: order/templates/order/purchase_order_detail.html:38 #: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:161 part/templates/part/category.html:202 +#: part/templates/part/category.html:171 part/templates/part/category.html:213 #: templates/js/stock.html:803 msgid "New Location" msgstr "Neuer Standort" @@ -1725,7 +1725,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:135 templates/js/part.html:388 +#: part/templates/part/part_base.html:145 templates/js/part.html:388 msgid "On Order" msgstr "bestellt" @@ -1823,7 +1823,7 @@ msgstr "Bestellungspositionen" msgid "Add Purchase Order Attachment" msgstr "Bestellanhang hinzufügen" -#: order/views.py:102 order/views.py:149 part/views.py:86 stock/views.py:167 +#: order/views.py:102 order/views.py:149 part/views.py:90 stock/views.py:167 msgid "Added attachment" msgstr "Anhang hinzugefügt" @@ -1963,12 +1963,12 @@ msgstr "Zuordnung bearbeiten" msgid "Remove allocation" msgstr "Zuordnung entfernen" -#: part/bom.py:138 part/templates/part/category.html:55 +#: part/bom.py:138 part/templates/part/category.html:61 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "Standard-Lagerort" -#: part/bom.py:139 part/templates/part/part_base.html:108 +#: part/bom.py:139 part/templates/part/part_base.html:118 msgid "Available Stock" msgstr "Verfügbarer Lagerbestand" @@ -2100,7 +2100,7 @@ msgid "Part Category" msgstr "Teilkategorie" #: part/models.py:76 part/templates/part/category.html:18 -#: part/templates/part/category.html:83 templates/stats.html:12 +#: part/templates/part/category.html:89 templates/stats.html:12 msgid "Part Categories" msgstr "Teile-Kategorien" @@ -2339,7 +2339,7 @@ msgstr "Notizen zum Stücklisten-Objekt" msgid "BOM line checksum" msgstr "Prüfsumme der Stückliste" -#: part/models.py:1612 part/views.py:1310 part/views.py:1362 +#: part/models.py:1612 part/views.py:1349 part/views.py:1401 #: stock/models.py:231 #, fuzzy #| msgid "Overage must be an integer value or a percentage" @@ -2402,25 +2402,25 @@ msgstr "Neue Stücklistenposition" msgid "Finish Editing" msgstr "Bearbeitung beenden" -#: part/templates/part/bom.html:42 +#: part/templates/part/bom.html:43 msgid "Edit BOM" msgstr "Stückliste bearbeiten" -#: part/templates/part/bom.html:44 +#: part/templates/part/bom.html:45 msgid "Validate Bill of Materials" msgstr "Stückliste validieren" -#: part/templates/part/bom.html:46 part/views.py:1597 +#: part/templates/part/bom.html:48 part/views.py:1640 msgid "Export Bill of Materials" msgstr "Stückliste exportieren" -#: part/templates/part/bom.html:101 +#: part/templates/part/bom.html:103 #, fuzzy #| msgid "Remove selected BOM items" msgid "Delete selected BOM items?" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: part/templates/part/bom.html:102 +#: part/templates/part/bom.html:104 #, fuzzy #| msgid "Remove selected BOM items" msgid "All selected BOM items will be deleted" @@ -2510,101 +2510,113 @@ msgstr "Neues Bild hochladen" msgid "Each part must already exist in the database" msgstr "" +#: part/templates/part/build.html:8 +#, fuzzy +#| msgid "Parent Build" +msgid "Part Builds" +msgstr "Eltern-Bau" + +#: part/templates/part/build.html:14 +#, fuzzy +#| msgid "Start new Build" +msgid "Start New Build" +msgstr "Neuen Bau beginnen" + #: part/templates/part/category.html:19 msgid "All parts" msgstr "Alle Teile" -#: part/templates/part/category.html:23 part/views.py:1976 +#: part/templates/part/category.html:24 part/views.py:2043 msgid "Create new part category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:27 +#: part/templates/part/category.html:30 #, fuzzy #| msgid "Edit Part Category" msgid "Edit part category" msgstr "Teilkategorie bearbeiten" -#: part/templates/part/category.html:30 +#: part/templates/part/category.html:35 #, fuzzy #| msgid "Select part category" msgid "Delete part category" msgstr "Teilekategorie wählen" -#: part/templates/part/category.html:39 part/templates/part/category.html:78 +#: part/templates/part/category.html:45 part/templates/part/category.html:84 msgid "Category Details" msgstr "Kategorie-Details" -#: part/templates/part/category.html:44 +#: part/templates/part/category.html:50 msgid "Category Path" msgstr "Pfad zur Kategorie" -#: part/templates/part/category.html:49 +#: part/templates/part/category.html:55 msgid "Category Description" msgstr "Kategorie-Beschreibung" -#: part/templates/part/category.html:62 part/templates/part/detail.html:64 +#: part/templates/part/category.html:68 part/templates/part/detail.html:64 msgid "Keywords" msgstr "Schlüsselwörter" -#: part/templates/part/category.html:68 +#: part/templates/part/category.html:74 msgid "Subcategories" msgstr "Unter-Kategorien" -#: part/templates/part/category.html:73 +#: part/templates/part/category.html:79 msgid "Parts (Including subcategories)" msgstr "Teile (inklusive Unter-Kategorien)" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:112 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:107 part/views.py:491 +#: part/templates/part/category.html:114 part/views.py:511 msgid "Create new part" msgstr "Neues Teil anlegen" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 #, fuzzy #| msgid "Part category" msgid "Set category" msgstr "Teile-Kategorie" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 #, fuzzy #| msgid "Set Part Category" msgid "Set Category" msgstr "Teilkategorie auswählen" -#: part/templates/part/category.html:113 +#: part/templates/part/category.html:123 #, fuzzy #| msgid "Export" msgid "Export Data" msgstr "Exportieren" -#: part/templates/part/category.html:162 +#: part/templates/part/category.html:172 #, fuzzy #| msgid "Create New Location" msgid "Create new location" msgstr "Neuen Standort anlegen" -#: part/templates/part/category.html:167 part/templates/part/category.html:196 +#: part/templates/part/category.html:177 part/templates/part/category.html:207 #, fuzzy #| msgid "Category" msgid "New Category" msgstr "Kategorie" -#: part/templates/part/category.html:168 +#: part/templates/part/category.html:178 #, fuzzy #| msgid "Create new part category" msgid "Create new category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:197 +#: part/templates/part/category.html:208 #, fuzzy #| msgid "Create new part category" msgid "Create new Part Category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:203 stock/views.py:1314 +#: part/templates/part/category.html:214 stock/views.py:1314 msgid "Create new Stock Location" msgstr "Neuen Lager-Standort erstellen" @@ -2618,7 +2630,7 @@ msgstr "Parameter Wert" msgid "Part Details" msgstr "Teile-Details" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:85 +#: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 #: templates/js/part.html:112 msgid "IPN" msgstr "IPN (Interne Produktnummer)" @@ -2652,7 +2664,7 @@ msgstr "Kategorie" msgid "Default Supplier" msgstr "Standard-Zulieferer" -#: part/templates/part/detail.html:102 part/templates/part/params.html:22 +#: part/templates/part/detail.html:102 part/templates/part/params.html:24 msgid "Units" msgstr "Einheiten" @@ -2785,24 +2797,25 @@ msgstr "Teil bestellen" msgid "Part Parameters" msgstr "Teilparameter" -#: part/templates/part/params.html:13 +#: part/templates/part/params.html:14 msgid "Add new parameter" msgstr "Parameter hinzufügen" -#: part/templates/part/params.html:13 templates/InvenTree/settings/part.html:12 +#: part/templates/part/params.html:14 templates/InvenTree/settings/part.html:12 msgid "New Parameter" msgstr "Neuer Parameter" -#: part/templates/part/params.html:21 stock/models.py:1391 +#: part/templates/part/params.html:23 stock/models.py:1391 #: templates/js/stock.html:112 msgid "Value" msgstr "Wert" -#: part/templates/part/params.html:33 +#: part/templates/part/params.html:36 msgid "Edit" msgstr "Bearbeiten" -#: part/templates/part/params.html:34 part/templates/part/supplier.html:17 +#: part/templates/part/params.html:39 part/templates/part/supplier.html:17 +#: users/models.py:141 msgid "Delete" msgstr "Löschen" @@ -2859,47 +2872,53 @@ msgstr "" msgid "Show pricing information" msgstr "Kosteninformationen ansehen" -#: part/templates/part/part_base.html:70 +#: part/templates/part/part_base.html:60 +#, fuzzy +#| msgid "Count stock" +msgid "Count part stock" +msgstr "Bestand zählen" + +#: part/templates/part/part_base.html:75 #, fuzzy #| msgid "Source Location" msgid "Part actions" msgstr "Quell-Standort" -#: part/templates/part/part_base.html:72 +#: part/templates/part/part_base.html:78 #, fuzzy #| msgid "Duplicate Part" msgid "Duplicate part" msgstr "Teil duplizieren" -#: part/templates/part/part_base.html:73 +#: part/templates/part/part_base.html:81 #, fuzzy #| msgid "Edit Template" msgid "Edit part" msgstr "Vorlage bearbeiten" -#: part/templates/part/part_base.html:75 +#: part/templates/part/part_base.html:84 #, fuzzy #| msgid "Delete Parts" msgid "Delete part" msgstr "Teile löschen" -#: part/templates/part/part_base.html:114 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:124 templates/js/table_filters.html:65 msgid "In Stock" msgstr "Auf Lager" -#: part/templates/part/part_base.html:121 +#: part/templates/part/part_base.html:131 msgid "Allocated to Build Orders" msgstr "Zu Bauaufträgen zugeordnet" -#: part/templates/part/part_base.html:128 +#: part/templates/part/part_base.html:138 msgid "Allocated to Sales Orders" msgstr "Zu Aufträgen zugeordnet" -#: part/templates/part/part_base.html:150 +#: part/templates/part/part_base.html:160 msgid "Can Build" msgstr "Herstellbar?" -#: part/templates/part/part_base.html:156 +#: part/templates/part/part_base.html:166 msgid "Underway" msgstr "unterwegs" @@ -2923,7 +2942,7 @@ msgstr "Aus vorhandenen Bildern auswählen" msgid "Upload new image" msgstr "Neues Bild hochladen" -#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:50 +#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:53 #, fuzzy #| msgid "Price" msgid "Sale Price" @@ -2994,11 +3013,11 @@ msgstr "Varianten" msgid "BOM" msgstr "Stückliste" -#: part/templates/part/tabs.html:34 +#: part/templates/part/tabs.html:37 msgid "Used In" msgstr "Benutzt in" -#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:282 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:282 msgid "Tests" msgstr "" @@ -3032,184 +3051,184 @@ msgstr "Neues Teil hinzufügen" msgid "New Variant" msgstr "Varianten" -#: part/views.py:76 +#: part/views.py:78 msgid "Add part attachment" msgstr "Teilanhang hinzufügen" -#: part/views.py:125 templates/attachment_table.html:30 +#: part/views.py:129 templates/attachment_table.html:30 msgid "Edit attachment" msgstr "Anhang bearbeiten" -#: part/views.py:129 +#: part/views.py:135 msgid "Part attachment updated" msgstr "Teilanhang aktualisiert" -#: part/views.py:144 +#: part/views.py:150 msgid "Delete Part Attachment" msgstr "Teilanhang löschen" -#: part/views.py:150 +#: part/views.py:158 msgid "Deleted part attachment" msgstr "Teilanhang gelöscht" -#: part/views.py:159 +#: part/views.py:167 #, fuzzy #| msgid "Create Part Parameter Template" msgid "Create Test Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:186 +#: part/views.py:196 #, fuzzy #| msgid "Edit Template" msgid "Edit Test Template" msgstr "Vorlage bearbeiten" -#: part/views.py:200 +#: part/views.py:212 #, fuzzy #| msgid "Delete Template" msgid "Delete Test Template" msgstr "Vorlage löschen" -#: part/views.py:207 +#: part/views.py:221 msgid "Set Part Category" msgstr "Teilkategorie auswählen" -#: part/views.py:255 +#: part/views.py:271 #, python-brace-format msgid "Set category for {n} parts" msgstr "Kategorie für {n} Teile setzen" -#: part/views.py:290 +#: part/views.py:306 msgid "Create Variant" msgstr "Variante anlegen" -#: part/views.py:368 +#: part/views.py:386 msgid "Duplicate Part" msgstr "Teil duplizieren" -#: part/views.py:373 +#: part/views.py:393 msgid "Copied part" msgstr "Teil kopiert" -#: part/views.py:496 +#: part/views.py:518 msgid "Created new part" msgstr "Neues Teil angelegt" -#: part/views.py:707 +#: part/views.py:733 msgid "Part QR Code" msgstr "Teil-QR-Code" -#: part/views.py:724 +#: part/views.py:752 msgid "Upload Part Image" msgstr "Teilbild hochladen" -#: part/views.py:729 part/views.py:764 +#: part/views.py:760 part/views.py:797 msgid "Updated part image" msgstr "Teilbild aktualisiert" -#: part/views.py:738 +#: part/views.py:769 msgid "Select Part Image" msgstr "Teilbild auswählen" -#: part/views.py:767 +#: part/views.py:800 msgid "Part image not found" msgstr "Teilbild nicht gefunden" -#: part/views.py:778 +#: part/views.py:811 msgid "Edit Part Properties" msgstr "Teileigenschaften bearbeiten" -#: part/views.py:800 +#: part/views.py:835 msgid "Validate BOM" msgstr "BOM validieren" -#: part/views.py:963 +#: part/views.py:1002 msgid "No BOM file provided" msgstr "Keine Stückliste angegeben" -#: part/views.py:1313 +#: part/views.py:1352 msgid "Enter a valid quantity" msgstr "Bitte eine gültige Anzahl eingeben" -#: part/views.py:1338 part/views.py:1341 +#: part/views.py:1377 part/views.py:1380 msgid "Select valid part" msgstr "Bitte ein gültiges Teil auswählen" -#: part/views.py:1347 +#: part/views.py:1386 msgid "Duplicate part selected" msgstr "Teil doppelt ausgewählt" -#: part/views.py:1385 +#: part/views.py:1424 msgid "Select a part" msgstr "Teil auswählen" -#: part/views.py:1391 +#: part/views.py:1430 #, fuzzy #| msgid "Select part to be used in BOM" msgid "Selected part creates a circular BOM" msgstr "Teil für die Nutzung in der Stückliste auswählen" -#: part/views.py:1395 +#: part/views.py:1434 msgid "Specify quantity" msgstr "Anzahl angeben" -#: part/views.py:1645 +#: part/views.py:1690 msgid "Confirm Part Deletion" msgstr "Löschen des Teils bestätigen" -#: part/views.py:1652 +#: part/views.py:1699 msgid "Part was deleted" msgstr "Teil wurde gelöscht" -#: part/views.py:1661 +#: part/views.py:1708 msgid "Part Pricing" msgstr "Teilbepreisung" -#: part/views.py:1783 +#: part/views.py:1834 msgid "Create Part Parameter Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:1791 +#: part/views.py:1844 msgid "Edit Part Parameter Template" msgstr "Teilparametervorlage bearbeiten" -#: part/views.py:1798 +#: part/views.py:1853 msgid "Delete Part Parameter Template" msgstr "Teilparametervorlage löschen" -#: part/views.py:1806 +#: part/views.py:1863 msgid "Create Part Parameter" msgstr "Teilparameter anlegen" -#: part/views.py:1856 +#: part/views.py:1915 msgid "Edit Part Parameter" msgstr "Teilparameter bearbeiten" -#: part/views.py:1870 +#: part/views.py:1931 msgid "Delete Part Parameter" msgstr "Teilparameter löschen" -#: part/views.py:1927 +#: part/views.py:1990 msgid "Edit Part Category" msgstr "Teilkategorie bearbeiten" -#: part/views.py:1962 +#: part/views.py:2027 msgid "Delete Part Category" msgstr "Teilkategorie löschen" -#: part/views.py:1968 +#: part/views.py:2035 msgid "Part category was deleted" msgstr "Teilekategorie wurde gelöscht" -#: part/views.py:2027 +#: part/views.py:2098 msgid "Create BOM item" msgstr "BOM-Position anlegen" -#: part/views.py:2093 +#: part/views.py:2166 msgid "Edit BOM item" msgstr "BOM-Position beaarbeiten" -#: part/views.py:2141 +#: part/views.py:2216 msgid "Confim BOM item deletion" msgstr "Löschung von BOM-Position bestätigen" @@ -3653,15 +3672,15 @@ msgid "Stock adjustment actions" msgstr "Bestands-Anpassung bestätigen" #: stock/templates/stock/item_base.html:98 -#: stock/templates/stock/location.html:38 templates/stock_table.html:15 +#: stock/templates/stock/location.html:38 templates/stock_table.html:19 msgid "Count stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:99 templates/stock_table.html:13 +#: stock/templates/stock/item_base.html:99 templates/stock_table.html:17 msgid "Add stock" msgstr "Bestand hinzufügen" -#: stock/templates/stock/item_base.html:100 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:100 templates/stock_table.html:18 msgid "Remove stock" msgstr "Bestand entfernen" @@ -4191,6 +4210,14 @@ msgstr "Lagerbestands-Tracking-Eintrag bearbeiten" msgid "Add Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag hinzufügen" +#: templates/403.html:5 templates/403.html:11 +msgid "Permission Denied" +msgstr "" + +#: templates/403.html:14 +msgid "You do not have permission to view this page." +msgstr "" + #: templates/InvenTree/bom_invalid.html:7 msgid "BOM Waiting Validation" msgstr "" @@ -4201,6 +4228,10 @@ msgstr "" msgid "Pending Builds" msgstr "Eltern-Bau" +#: templates/InvenTree/index.html:4 +msgid "Index" +msgstr "" + #: templates/InvenTree/latest_parts.html:7 #, fuzzy #| msgid "Parent Part" @@ -4884,39 +4915,39 @@ msgstr "Favorit" msgid "Purchasable" msgstr "Käuflich" -#: templates/navbar.html:22 +#: templates/navbar.html:29 msgid "Buy" msgstr "Kaufen" -#: templates/navbar.html:30 +#: templates/navbar.html:39 msgid "Sell" msgstr "Verkaufen" -#: templates/navbar.html:40 +#: templates/navbar.html:50 msgid "Scan Barcode" msgstr "" -#: templates/navbar.html:49 +#: templates/navbar.html:59 users/models.py:27 msgid "Admin" msgstr "Admin" -#: templates/navbar.html:52 +#: templates/navbar.html:62 msgid "Settings" msgstr "Einstellungen" -#: templates/navbar.html:53 +#: templates/navbar.html:63 msgid "Logout" msgstr "Ausloggen" -#: templates/navbar.html:55 +#: templates/navbar.html:65 msgid "Login" msgstr "Einloggen" -#: templates/navbar.html:58 +#: templates/navbar.html:68 msgid "About InvenTree" msgstr "Über InvenBaum" -#: templates/navbar.html:59 +#: templates/navbar.html:69 msgid "Statistics" msgstr "Statistiken" @@ -4930,54 +4961,124 @@ msgstr "Suche" msgid "Export Stock Information" msgstr "Lagerobjekt-Standort bearbeiten" -#: templates/stock_table.html:13 +#: templates/stock_table.html:17 #, fuzzy #| msgid "Added stock to {n} items" msgid "Add to selected stock items" msgstr "Vorrat zu {n} Lagerobjekten hinzugefügt" -#: templates/stock_table.html:14 +#: templates/stock_table.html:18 #, fuzzy #| msgid "Remove selected BOM items" msgid "Remove from selected stock items" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: templates/stock_table.html:15 +#: templates/stock_table.html:19 #, fuzzy #| msgid "Delete Stock Item" msgid "Stocktake selected stock items" msgstr "Lagerobjekt löschen" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 #, fuzzy #| msgid "Delete Stock Item" msgid "Move selected stock items" msgstr "Lagerobjekt löschen" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 msgid "Move stock" msgstr "Bestand bewegen" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 #, fuzzy #| msgid "Remove selected BOM items" msgid "Order selected items" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Order stock" msgstr "Bestand bestellen" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 #, fuzzy #| msgid "Delete line item" msgid "Delete selected items" msgstr "Position löschen" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 msgid "Delete Stock" msgstr "Bestand löschen" +#: users/admin.py:62 +#, fuzzy +#| msgid "User" +msgid "Users" +msgstr "Benutzer" + +#: users/admin.py:63 +msgid "Select which users are assigned to this group" +msgstr "" + +#: users/admin.py:124 +#, fuzzy +#| msgid "External Link" +msgid "Personal info" +msgstr "Externer Link" + +#: users/admin.py:125 +#, fuzzy +#| msgid "Revision" +msgid "Permissions" +msgstr "Revision" + +#: users/admin.py:128 +#, fuzzy +#| msgid "Import BOM data" +msgid "Important dates" +msgstr "Stückliste importieren" + +#: users/models.py:124 +msgid "Permission set" +msgstr "" + +#: users/models.py:132 +msgid "Group" +msgstr "" + +#: users/models.py:135 +msgid "View" +msgstr "" + +#: users/models.py:135 +msgid "Permission to view items" +msgstr "" + +#: users/models.py:137 +#, fuzzy +#| msgid "Created" +msgid "Create" +msgstr "Erstellt" + +#: users/models.py:137 +msgid "Permission to add items" +msgstr "" + +#: users/models.py:139 +#, fuzzy +#| msgid "Last Updated" +msgid "Update" +msgstr "Zuletzt aktualisiert" + +#: users/models.py:139 +msgid "Permissions to edit items" +msgstr "" + +#: users/models.py:141 +#, fuzzy +#| msgid "Remove selected BOM items" +msgid "Permission to delete items" +msgstr "Ausgewählte Stücklistenpositionen entfernen" + #~ msgid "Belongs To" #~ msgstr "Gehört zu" diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index 0b5425db6e..0f38022f92 100644 --- a/InvenTree/locale/en/LC_MESSAGES/django.po +++ b/InvenTree/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-04 14:02+0000\n" +"POT-Creation-Date: 2020-10-05 13:20+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -90,7 +90,7 @@ msgstr "" msgid "User" msgstr "" -#: InvenTree/models.py:106 part/templates/part/params.html:20 +#: InvenTree/models.py:106 part/templates/part/params.html:22 #: templates/js/part.html:81 msgid "Name" msgstr "" @@ -99,19 +99,19 @@ msgstr "" msgid "Description (optional)" msgstr "" -#: InvenTree/settings.py:342 +#: InvenTree/settings.py:343 msgid "English" msgstr "" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:344 msgid "German" msgstr "" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:345 msgid "French" msgstr "" -#: InvenTree/settings.py:345 +#: InvenTree/settings.py:346 msgid "Polish" msgstr "" @@ -325,7 +325,7 @@ msgstr "" msgid "Number of parts to build" msgstr "" -#: build/models.py:128 part/templates/part/part_base.html:145 +#: build/models.py:128 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "" @@ -344,7 +344,7 @@ msgstr "" #: build/models.py:155 build/templates/build/detail.html:55 #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 -#: part/templates/part/detail.html:80 part/templates/part/part_base.html:92 +#: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 #: stock/models.py:381 stock/templates/stock/item_base.html:244 msgid "External Link" msgstr "" @@ -356,7 +356,7 @@ msgstr "" #: build/models.py:160 build/templates/build/tabs.html:14 company/models.py:310 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:15 #: order/templates/order/purchase_order_detail.html:202 -#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:67 +#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:70 #: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 #: stock/models.py:1404 stock/templates/stock/tabs.html:26 #: templates/js/barcode.html:391 templates/js/bom.html:223 @@ -404,7 +404,7 @@ msgstr "" #: build/templates/build/allocate.html:17 #: company/templates/company/detail_part.html:18 order/views.py:779 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 msgid "Order Parts" msgstr "" @@ -420,7 +420,7 @@ msgstr "" msgid "Unallocate" msgstr "" -#: build/templates/build/allocate.html:87 templates/stock_table.html:9 +#: build/templates/build/allocate.html:87 templates/stock_table.html:10 msgid "New Stock Item" msgstr "" @@ -548,7 +548,7 @@ msgstr "" #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 #: stock/templates/stock/item_base.html:223 templates/js/build.html:39 -#: templates/navbar.html:20 +#: templates/navbar.html:25 msgid "Build" msgstr "" @@ -687,7 +687,7 @@ msgstr "" #: build/templates/build/index.html:6 build/templates/build/index.html:14 #: order/templates/order/so_builds.html:11 order/templates/order/so_tabs.html:9 -#: part/templates/part/tabs.html:30 +#: part/templates/part/tabs.html:31 users/models.py:30 msgid "Build Orders" msgstr "" @@ -709,7 +709,7 @@ msgstr "" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 #: order/templates/order/order_notes.html:32 #: order/templates/order/sales_order_notes.html:37 -#: part/templates/part/notes.html:32 stock/templates/stock/item_notes.html:33 +#: part/templates/part/notes.html:33 stock/templates/stock/item_notes.html:33 msgid "Edit notes" msgstr "" @@ -1044,13 +1044,13 @@ msgid "New Supplier Part" msgstr "" #: company/templates/company/detail_part.html:15 -#: part/templates/part/category.html:109 part/templates/part/supplier.html:15 -#: templates/stock_table.html:11 +#: part/templates/part/category.html:117 part/templates/part/supplier.html:15 +#: templates/stock_table.html:14 msgid "Options" msgstr "" #: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 msgid "Order parts" msgstr "" @@ -1063,7 +1063,7 @@ msgid "Delete Parts" msgstr "" #: company/templates/company/detail_part.html:43 -#: part/templates/part/category.html:107 templates/js/stock.html:791 +#: part/templates/part/category.html:114 templates/js/stock.html:791 msgid "New Part" msgstr "" @@ -1095,7 +1095,7 @@ msgstr "" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:106 part/templates/part/category.html:113 +#: part/templates/part/category.html:112 part/templates/part/category.html:123 #: part/templates/part/stock.html:51 templates/stock_table.html:6 msgid "Export" msgstr "" @@ -1117,8 +1117,8 @@ msgstr "" #: company/templates/company/tabs.html:17 #: order/templates/order/purchase_orders.html:7 #: order/templates/order/purchase_orders.html:12 -#: part/templates/part/orders.html:9 part/templates/part/tabs.html:45 -#: templates/navbar.html:26 +#: part/templates/part/orders.html:9 part/templates/part/tabs.html:48 +#: templates/navbar.html:33 users/models.py:31 msgid "Purchase Orders" msgstr "" @@ -1136,8 +1136,8 @@ msgstr "" #: company/templates/company/tabs.html:22 #: order/templates/order/sales_orders.html:7 #: order/templates/order/sales_orders.html:12 -#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:53 -#: templates/navbar.html:33 +#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:56 +#: templates/navbar.html:42 users/models.py:32 msgid "Sales Orders" msgstr "" @@ -1158,7 +1158,7 @@ msgid "Supplier Part" msgstr "" #: company/templates/company/supplier_part_base.html:23 -#: part/templates/part/orders.html:14 +#: part/templates/part/orders.html:14 part/templates/part/part_base.html:66 msgid "Order part" msgstr "" @@ -1205,7 +1205,7 @@ msgid "Pricing Information" msgstr "" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2149 +#: part/templates/part/sale_prices.html:13 part/views.py:2226 msgid "Add Price Break" msgstr "" @@ -1241,7 +1241,7 @@ msgstr "" #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 #: templates/js/part.html:124 templates/js/part.html:372 -#: templates/js/stock.html:452 templates/navbar.html:19 +#: templates/js/stock.html:452 templates/navbar.html:22 users/models.py:29 msgid "Stock" msgstr "" @@ -1251,22 +1251,22 @@ msgstr "" #: company/templates/company/tabs.html:9 #: order/templates/order/receive_parts.html:14 part/models.py:294 -#: part/templates/part/cat_link.html:7 part/templates/part/category.html:88 -#: part/templates/part/category_tabs.html:6 templates/navbar.html:18 -#: templates/stats.html:8 templates/stats.html:17 +#: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 +#: part/templates/part/category_tabs.html:6 templates/navbar.html:19 +#: templates/stats.html:8 templates/stats.html:17 users/models.py:28 msgid "Parts" msgstr "" -#: company/views.py:50 part/templates/part/tabs.html:39 -#: templates/navbar.html:24 +#: company/views.py:50 part/templates/part/tabs.html:42 +#: templates/navbar.html:31 msgid "Suppliers" msgstr "" -#: company/views.py:57 templates/navbar.html:25 +#: company/views.py:57 templates/navbar.html:32 msgid "Manufacturers" msgstr "" -#: company/views.py:64 templates/navbar.html:32 +#: company/views.py:64 templates/navbar.html:41 msgid "Customers" msgstr "" @@ -1330,15 +1330,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:404 part/views.py:2153 +#: company/views.py:404 part/views.py:2232 msgid "Added new price break" msgstr "" -#: company/views.py:441 part/views.py:2198 +#: company/views.py:441 part/views.py:2277 msgid "Edit Price Break" msgstr "" -#: company/views.py:456 part/views.py:2212 +#: company/views.py:456 part/views.py:2293 msgid "Delete Price Break" msgstr "" @@ -1431,7 +1431,7 @@ msgstr "" msgid "Date order was completed" msgstr "" -#: order/models.py:185 order/models.py:259 part/views.py:1304 +#: order/models.py:185 order/models.py:259 part/views.py:1343 #: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "" @@ -1600,7 +1600,7 @@ msgid "Purchase Order Attachments" msgstr "" #: order/templates/order/po_tabs.html:8 order/templates/order/so_tabs.html:16 -#: part/templates/part/tabs.html:64 stock/templates/stock/tabs.html:32 +#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 msgid "Attachments" msgstr "" @@ -1616,7 +1616,7 @@ msgstr "" #: order/templates/order/purchase_order_detail.html:38 #: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:161 part/templates/part/category.html:202 +#: part/templates/part/category.html:171 part/templates/part/category.html:213 #: templates/js/stock.html:803 msgid "New Location" msgstr "" @@ -1658,7 +1658,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:135 templates/js/part.html:388 +#: part/templates/part/part_base.html:145 templates/js/part.html:388 msgid "On Order" msgstr "" @@ -1750,7 +1750,7 @@ msgstr "" msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:102 order/views.py:149 part/views.py:86 stock/views.py:167 +#: order/views.py:102 order/views.py:149 part/views.py:90 stock/views.py:167 msgid "Added attachment" msgstr "" @@ -1890,12 +1890,12 @@ msgstr "" msgid "Remove allocation" msgstr "" -#: part/bom.py:138 part/templates/part/category.html:55 +#: part/bom.py:138 part/templates/part/category.html:61 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "" -#: part/bom.py:139 part/templates/part/part_base.html:108 +#: part/bom.py:139 part/templates/part/part_base.html:118 msgid "Available Stock" msgstr "" @@ -2013,7 +2013,7 @@ msgid "Part Category" msgstr "" #: part/models.py:76 part/templates/part/category.html:18 -#: part/templates/part/category.html:83 templates/stats.html:12 +#: part/templates/part/category.html:89 templates/stats.html:12 msgid "Part Categories" msgstr "" @@ -2226,7 +2226,7 @@ msgstr "" msgid "BOM line checksum" msgstr "" -#: part/models.py:1612 part/views.py:1310 part/views.py:1362 +#: part/models.py:1612 part/views.py:1349 part/views.py:1401 #: stock/models.py:231 msgid "Quantity must be integer value for trackable parts" msgstr "" @@ -2285,23 +2285,23 @@ msgstr "" msgid "Finish Editing" msgstr "" -#: part/templates/part/bom.html:42 +#: part/templates/part/bom.html:43 msgid "Edit BOM" msgstr "" -#: part/templates/part/bom.html:44 +#: part/templates/part/bom.html:45 msgid "Validate Bill of Materials" msgstr "" -#: part/templates/part/bom.html:46 part/views.py:1597 +#: part/templates/part/bom.html:48 part/views.py:1640 msgid "Export Bill of Materials" msgstr "" -#: part/templates/part/bom.html:101 +#: part/templates/part/bom.html:103 msgid "Delete selected BOM items?" msgstr "" -#: part/templates/part/bom.html:102 +#: part/templates/part/bom.html:104 msgid "All selected BOM items will be deleted" msgstr "" @@ -2373,83 +2373,91 @@ msgstr "" msgid "Each part must already exist in the database" msgstr "" +#: part/templates/part/build.html:8 +msgid "Part Builds" +msgstr "" + +#: part/templates/part/build.html:14 +msgid "Start New Build" +msgstr "" + #: part/templates/part/category.html:19 msgid "All parts" msgstr "" -#: part/templates/part/category.html:23 part/views.py:1976 +#: part/templates/part/category.html:24 part/views.py:2043 msgid "Create new part category" msgstr "" -#: part/templates/part/category.html:27 +#: part/templates/part/category.html:30 msgid "Edit part category" msgstr "" -#: part/templates/part/category.html:30 +#: part/templates/part/category.html:35 msgid "Delete part category" msgstr "" -#: part/templates/part/category.html:39 part/templates/part/category.html:78 +#: part/templates/part/category.html:45 part/templates/part/category.html:84 msgid "Category Details" msgstr "" -#: part/templates/part/category.html:44 +#: part/templates/part/category.html:50 msgid "Category Path" msgstr "" -#: part/templates/part/category.html:49 +#: part/templates/part/category.html:55 msgid "Category Description" msgstr "" -#: part/templates/part/category.html:62 part/templates/part/detail.html:64 +#: part/templates/part/category.html:68 part/templates/part/detail.html:64 msgid "Keywords" msgstr "" -#: part/templates/part/category.html:68 +#: part/templates/part/category.html:74 msgid "Subcategories" msgstr "" -#: part/templates/part/category.html:73 +#: part/templates/part/category.html:79 msgid "Parts (Including subcategories)" msgstr "" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:112 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:107 part/views.py:491 +#: part/templates/part/category.html:114 part/views.py:511 msgid "Create new part" msgstr "" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 msgid "Set category" msgstr "" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 msgid "Set Category" msgstr "" -#: part/templates/part/category.html:113 +#: part/templates/part/category.html:123 msgid "Export Data" msgstr "" -#: part/templates/part/category.html:162 +#: part/templates/part/category.html:172 msgid "Create new location" msgstr "" -#: part/templates/part/category.html:167 part/templates/part/category.html:196 +#: part/templates/part/category.html:177 part/templates/part/category.html:207 msgid "New Category" msgstr "" -#: part/templates/part/category.html:168 +#: part/templates/part/category.html:178 msgid "Create new category" msgstr "" -#: part/templates/part/category.html:197 +#: part/templates/part/category.html:208 msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:203 stock/views.py:1314 +#: part/templates/part/category.html:214 stock/views.py:1314 msgid "Create new Stock Location" msgstr "" @@ -2461,7 +2469,7 @@ msgstr "" msgid "Part Details" msgstr "" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:85 +#: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 #: templates/js/part.html:112 msgid "IPN" msgstr "" @@ -2491,7 +2499,7 @@ msgstr "" msgid "Default Supplier" msgstr "" -#: part/templates/part/detail.html:102 part/templates/part/params.html:22 +#: part/templates/part/detail.html:102 part/templates/part/params.html:24 msgid "Units" msgstr "" @@ -2616,24 +2624,25 @@ msgstr "" msgid "Part Parameters" msgstr "" -#: part/templates/part/params.html:13 +#: part/templates/part/params.html:14 msgid "Add new parameter" msgstr "" -#: part/templates/part/params.html:13 templates/InvenTree/settings/part.html:12 +#: part/templates/part/params.html:14 templates/InvenTree/settings/part.html:12 msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:21 stock/models.py:1391 +#: part/templates/part/params.html:23 stock/models.py:1391 #: templates/js/stock.html:112 msgid "Value" msgstr "" -#: part/templates/part/params.html:33 +#: part/templates/part/params.html:36 msgid "Edit" msgstr "" -#: part/templates/part/params.html:34 part/templates/part/supplier.html:17 +#: part/templates/part/params.html:39 part/templates/part/supplier.html:17 +#: users/models.py:141 msgid "Delete" msgstr "" @@ -2684,39 +2693,43 @@ msgstr "" msgid "Show pricing information" msgstr "" -#: part/templates/part/part_base.html:70 -msgid "Part actions" -msgstr "" - -#: part/templates/part/part_base.html:72 -msgid "Duplicate part" -msgstr "" - -#: part/templates/part/part_base.html:73 -msgid "Edit part" +#: part/templates/part/part_base.html:60 +msgid "Count part stock" msgstr "" #: part/templates/part/part_base.html:75 +msgid "Part actions" +msgstr "" + +#: part/templates/part/part_base.html:78 +msgid "Duplicate part" +msgstr "" + +#: part/templates/part/part_base.html:81 +msgid "Edit part" +msgstr "" + +#: part/templates/part/part_base.html:84 msgid "Delete part" msgstr "" -#: part/templates/part/part_base.html:114 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:124 templates/js/table_filters.html:65 msgid "In Stock" msgstr "" -#: part/templates/part/part_base.html:121 +#: part/templates/part/part_base.html:131 msgid "Allocated to Build Orders" msgstr "" -#: part/templates/part/part_base.html:128 +#: part/templates/part/part_base.html:138 msgid "Allocated to Sales Orders" msgstr "" -#: part/templates/part/part_base.html:150 +#: part/templates/part/part_base.html:160 msgid "Can Build" msgstr "" -#: part/templates/part/part_base.html:156 +#: part/templates/part/part_base.html:166 msgid "Underway" msgstr "" @@ -2736,7 +2749,7 @@ msgstr "" msgid "Upload new image" msgstr "" -#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:50 +#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:53 msgid "Sale Price" msgstr "" @@ -2797,11 +2810,11 @@ msgstr "" msgid "BOM" msgstr "" -#: part/templates/part/tabs.html:34 +#: part/templates/part/tabs.html:37 msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:282 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:282 msgid "Tests" msgstr "" @@ -2829,176 +2842,176 @@ msgstr "" msgid "New Variant" msgstr "" -#: part/views.py:76 +#: part/views.py:78 msgid "Add part attachment" msgstr "" -#: part/views.py:125 templates/attachment_table.html:30 +#: part/views.py:129 templates/attachment_table.html:30 msgid "Edit attachment" msgstr "" -#: part/views.py:129 +#: part/views.py:135 msgid "Part attachment updated" msgstr "" -#: part/views.py:144 +#: part/views.py:150 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:150 +#: part/views.py:158 msgid "Deleted part attachment" msgstr "" -#: part/views.py:159 +#: part/views.py:167 msgid "Create Test Template" msgstr "" -#: part/views.py:186 +#: part/views.py:196 msgid "Edit Test Template" msgstr "" -#: part/views.py:200 +#: part/views.py:212 msgid "Delete Test Template" msgstr "" -#: part/views.py:207 +#: part/views.py:221 msgid "Set Part Category" msgstr "" -#: part/views.py:255 +#: part/views.py:271 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:290 +#: part/views.py:306 msgid "Create Variant" msgstr "" -#: part/views.py:368 +#: part/views.py:386 msgid "Duplicate Part" msgstr "" -#: part/views.py:373 +#: part/views.py:393 msgid "Copied part" msgstr "" -#: part/views.py:496 +#: part/views.py:518 msgid "Created new part" msgstr "" -#: part/views.py:707 +#: part/views.py:733 msgid "Part QR Code" msgstr "" -#: part/views.py:724 +#: part/views.py:752 msgid "Upload Part Image" msgstr "" -#: part/views.py:729 part/views.py:764 +#: part/views.py:760 part/views.py:797 msgid "Updated part image" msgstr "" -#: part/views.py:738 +#: part/views.py:769 msgid "Select Part Image" msgstr "" -#: part/views.py:767 +#: part/views.py:800 msgid "Part image not found" msgstr "" -#: part/views.py:778 +#: part/views.py:811 msgid "Edit Part Properties" msgstr "" -#: part/views.py:800 +#: part/views.py:835 msgid "Validate BOM" msgstr "" -#: part/views.py:963 +#: part/views.py:1002 msgid "No BOM file provided" msgstr "" -#: part/views.py:1313 +#: part/views.py:1352 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1338 part/views.py:1341 +#: part/views.py:1377 part/views.py:1380 msgid "Select valid part" msgstr "" -#: part/views.py:1347 +#: part/views.py:1386 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1385 +#: part/views.py:1424 msgid "Select a part" msgstr "" -#: part/views.py:1391 +#: part/views.py:1430 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1395 +#: part/views.py:1434 msgid "Specify quantity" msgstr "" -#: part/views.py:1645 +#: part/views.py:1690 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1652 +#: part/views.py:1699 msgid "Part was deleted" msgstr "" -#: part/views.py:1661 +#: part/views.py:1708 msgid "Part Pricing" msgstr "" -#: part/views.py:1783 +#: part/views.py:1834 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1791 +#: part/views.py:1844 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1798 +#: part/views.py:1853 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1806 +#: part/views.py:1863 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1856 +#: part/views.py:1915 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:1870 +#: part/views.py:1931 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1927 +#: part/views.py:1990 msgid "Edit Part Category" msgstr "" -#: part/views.py:1962 +#: part/views.py:2027 msgid "Delete Part Category" msgstr "" -#: part/views.py:1968 +#: part/views.py:2035 msgid "Part category was deleted" msgstr "" -#: part/views.py:2027 +#: part/views.py:2098 msgid "Create BOM item" msgstr "" -#: part/views.py:2093 +#: part/views.py:2166 msgid "Edit BOM item" msgstr "" -#: part/views.py:2141 +#: part/views.py:2216 msgid "Confim BOM item deletion" msgstr "" @@ -3371,15 +3384,15 @@ msgid "Stock adjustment actions" msgstr "" #: stock/templates/stock/item_base.html:98 -#: stock/templates/stock/location.html:38 templates/stock_table.html:15 +#: stock/templates/stock/location.html:38 templates/stock_table.html:19 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:99 templates/stock_table.html:13 +#: stock/templates/stock/item_base.html:99 templates/stock_table.html:17 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:100 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:100 templates/stock_table.html:18 msgid "Remove stock" msgstr "" @@ -3819,6 +3832,14 @@ msgstr "" msgid "Add Stock Tracking Entry" msgstr "" +#: templates/403.html:5 templates/403.html:11 +msgid "Permission Denied" +msgstr "" + +#: templates/403.html:14 +msgid "You do not have permission to view this page." +msgstr "" + #: templates/InvenTree/bom_invalid.html:7 msgid "BOM Waiting Validation" msgstr "" @@ -3827,6 +3848,10 @@ msgstr "" msgid "Pending Builds" msgstr "" +#: templates/InvenTree/index.html:4 +msgid "Index" +msgstr "" + #: templates/InvenTree/latest_parts.html:7 msgid "Latest Parts" msgstr "" @@ -4392,39 +4417,39 @@ msgstr "" msgid "Purchasable" msgstr "" -#: templates/navbar.html:22 +#: templates/navbar.html:29 msgid "Buy" msgstr "" -#: templates/navbar.html:30 +#: templates/navbar.html:39 msgid "Sell" msgstr "" -#: templates/navbar.html:40 +#: templates/navbar.html:50 msgid "Scan Barcode" msgstr "" -#: templates/navbar.html:49 +#: templates/navbar.html:59 users/models.py:27 msgid "Admin" msgstr "" -#: templates/navbar.html:52 +#: templates/navbar.html:62 msgid "Settings" msgstr "" -#: templates/navbar.html:53 +#: templates/navbar.html:63 msgid "Logout" msgstr "" -#: templates/navbar.html:55 +#: templates/navbar.html:65 msgid "Login" msgstr "" -#: templates/navbar.html:58 +#: templates/navbar.html:68 msgid "About InvenTree" msgstr "" -#: templates/navbar.html:59 +#: templates/navbar.html:69 msgid "Statistics" msgstr "" @@ -4436,38 +4461,94 @@ msgstr "" msgid "Export Stock Information" msgstr "" -#: templates/stock_table.html:13 +#: templates/stock_table.html:17 msgid "Add to selected stock items" msgstr "" -#: templates/stock_table.html:14 +#: templates/stock_table.html:18 msgid "Remove from selected stock items" msgstr "" -#: templates/stock_table.html:15 +#: templates/stock_table.html:19 msgid "Stocktake selected stock items" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 msgid "Move selected stock items" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 msgid "Move stock" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Order selected items" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Order stock" msgstr "" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 msgid "Delete selected items" msgstr "" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 msgid "Delete Stock" msgstr "" + +#: users/admin.py:62 +msgid "Users" +msgstr "" + +#: users/admin.py:63 +msgid "Select which users are assigned to this group" +msgstr "" + +#: users/admin.py:124 +msgid "Personal info" +msgstr "" + +#: users/admin.py:125 +msgid "Permissions" +msgstr "" + +#: users/admin.py:128 +msgid "Important dates" +msgstr "" + +#: users/models.py:124 +msgid "Permission set" +msgstr "" + +#: users/models.py:132 +msgid "Group" +msgstr "" + +#: users/models.py:135 +msgid "View" +msgstr "" + +#: users/models.py:135 +msgid "Permission to view items" +msgstr "" + +#: users/models.py:137 +msgid "Create" +msgstr "" + +#: users/models.py:137 +msgid "Permission to add items" +msgstr "" + +#: users/models.py:139 +msgid "Update" +msgstr "" + +#: users/models.py:139 +msgid "Permissions to edit items" +msgstr "" + +#: users/models.py:141 +msgid "Permission to delete items" +msgstr "" diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po index 0b5425db6e..0f38022f92 100644 --- a/InvenTree/locale/es/LC_MESSAGES/django.po +++ b/InvenTree/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-04 14:02+0000\n" +"POT-Creation-Date: 2020-10-05 13:20+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -90,7 +90,7 @@ msgstr "" msgid "User" msgstr "" -#: InvenTree/models.py:106 part/templates/part/params.html:20 +#: InvenTree/models.py:106 part/templates/part/params.html:22 #: templates/js/part.html:81 msgid "Name" msgstr "" @@ -99,19 +99,19 @@ msgstr "" msgid "Description (optional)" msgstr "" -#: InvenTree/settings.py:342 +#: InvenTree/settings.py:343 msgid "English" msgstr "" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:344 msgid "German" msgstr "" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:345 msgid "French" msgstr "" -#: InvenTree/settings.py:345 +#: InvenTree/settings.py:346 msgid "Polish" msgstr "" @@ -325,7 +325,7 @@ msgstr "" msgid "Number of parts to build" msgstr "" -#: build/models.py:128 part/templates/part/part_base.html:145 +#: build/models.py:128 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "" @@ -344,7 +344,7 @@ msgstr "" #: build/models.py:155 build/templates/build/detail.html:55 #: company/templates/company/supplier_part_base.html:60 #: company/templates/company/supplier_part_detail.html:24 -#: part/templates/part/detail.html:80 part/templates/part/part_base.html:92 +#: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 #: stock/models.py:381 stock/templates/stock/item_base.html:244 msgid "External Link" msgstr "" @@ -356,7 +356,7 @@ msgstr "" #: build/models.py:160 build/templates/build/tabs.html:14 company/models.py:310 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:15 #: order/templates/order/purchase_order_detail.html:202 -#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:67 +#: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:70 #: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 #: stock/models.py:1404 stock/templates/stock/tabs.html:26 #: templates/js/barcode.html:391 templates/js/bom.html:223 @@ -404,7 +404,7 @@ msgstr "" #: build/templates/build/allocate.html:17 #: company/templates/company/detail_part.html:18 order/views.py:779 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 msgid "Order Parts" msgstr "" @@ -420,7 +420,7 @@ msgstr "" msgid "Unallocate" msgstr "" -#: build/templates/build/allocate.html:87 templates/stock_table.html:9 +#: build/templates/build/allocate.html:87 templates/stock_table.html:10 msgid "New Stock Item" msgstr "" @@ -548,7 +548,7 @@ msgstr "" #: build/templates/build/build_base.html:34 #: build/templates/build/complete.html:6 #: stock/templates/stock/item_base.html:223 templates/js/build.html:39 -#: templates/navbar.html:20 +#: templates/navbar.html:25 msgid "Build" msgstr "" @@ -687,7 +687,7 @@ msgstr "" #: build/templates/build/index.html:6 build/templates/build/index.html:14 #: order/templates/order/so_builds.html:11 order/templates/order/so_tabs.html:9 -#: part/templates/part/tabs.html:30 +#: part/templates/part/tabs.html:31 users/models.py:30 msgid "Build Orders" msgstr "" @@ -709,7 +709,7 @@ msgstr "" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 #: order/templates/order/order_notes.html:32 #: order/templates/order/sales_order_notes.html:37 -#: part/templates/part/notes.html:32 stock/templates/stock/item_notes.html:33 +#: part/templates/part/notes.html:33 stock/templates/stock/item_notes.html:33 msgid "Edit notes" msgstr "" @@ -1044,13 +1044,13 @@ msgid "New Supplier Part" msgstr "" #: company/templates/company/detail_part.html:15 -#: part/templates/part/category.html:109 part/templates/part/supplier.html:15 -#: templates/stock_table.html:11 +#: part/templates/part/category.html:117 part/templates/part/supplier.html:15 +#: templates/stock_table.html:14 msgid "Options" msgstr "" #: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:122 msgid "Order parts" msgstr "" @@ -1063,7 +1063,7 @@ msgid "Delete Parts" msgstr "" #: company/templates/company/detail_part.html:43 -#: part/templates/part/category.html:107 templates/js/stock.html:791 +#: part/templates/part/category.html:114 templates/js/stock.html:791 msgid "New Part" msgstr "" @@ -1095,7 +1095,7 @@ msgstr "" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:106 part/templates/part/category.html:113 +#: part/templates/part/category.html:112 part/templates/part/category.html:123 #: part/templates/part/stock.html:51 templates/stock_table.html:6 msgid "Export" msgstr "" @@ -1117,8 +1117,8 @@ msgstr "" #: company/templates/company/tabs.html:17 #: order/templates/order/purchase_orders.html:7 #: order/templates/order/purchase_orders.html:12 -#: part/templates/part/orders.html:9 part/templates/part/tabs.html:45 -#: templates/navbar.html:26 +#: part/templates/part/orders.html:9 part/templates/part/tabs.html:48 +#: templates/navbar.html:33 users/models.py:31 msgid "Purchase Orders" msgstr "" @@ -1136,8 +1136,8 @@ msgstr "" #: company/templates/company/tabs.html:22 #: order/templates/order/sales_orders.html:7 #: order/templates/order/sales_orders.html:12 -#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:53 -#: templates/navbar.html:33 +#: part/templates/part/sales_orders.html:9 part/templates/part/tabs.html:56 +#: templates/navbar.html:42 users/models.py:32 msgid "Sales Orders" msgstr "" @@ -1158,7 +1158,7 @@ msgid "Supplier Part" msgstr "" #: company/templates/company/supplier_part_base.html:23 -#: part/templates/part/orders.html:14 +#: part/templates/part/orders.html:14 part/templates/part/part_base.html:66 msgid "Order part" msgstr "" @@ -1205,7 +1205,7 @@ msgid "Pricing Information" msgstr "" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2149 +#: part/templates/part/sale_prices.html:13 part/views.py:2226 msgid "Add Price Break" msgstr "" @@ -1241,7 +1241,7 @@ msgstr "" #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 #: templates/js/part.html:124 templates/js/part.html:372 -#: templates/js/stock.html:452 templates/navbar.html:19 +#: templates/js/stock.html:452 templates/navbar.html:22 users/models.py:29 msgid "Stock" msgstr "" @@ -1251,22 +1251,22 @@ msgstr "" #: company/templates/company/tabs.html:9 #: order/templates/order/receive_parts.html:14 part/models.py:294 -#: part/templates/part/cat_link.html:7 part/templates/part/category.html:88 -#: part/templates/part/category_tabs.html:6 templates/navbar.html:18 -#: templates/stats.html:8 templates/stats.html:17 +#: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 +#: part/templates/part/category_tabs.html:6 templates/navbar.html:19 +#: templates/stats.html:8 templates/stats.html:17 users/models.py:28 msgid "Parts" msgstr "" -#: company/views.py:50 part/templates/part/tabs.html:39 -#: templates/navbar.html:24 +#: company/views.py:50 part/templates/part/tabs.html:42 +#: templates/navbar.html:31 msgid "Suppliers" msgstr "" -#: company/views.py:57 templates/navbar.html:25 +#: company/views.py:57 templates/navbar.html:32 msgid "Manufacturers" msgstr "" -#: company/views.py:64 templates/navbar.html:32 +#: company/views.py:64 templates/navbar.html:41 msgid "Customers" msgstr "" @@ -1330,15 +1330,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:404 part/views.py:2153 +#: company/views.py:404 part/views.py:2232 msgid "Added new price break" msgstr "" -#: company/views.py:441 part/views.py:2198 +#: company/views.py:441 part/views.py:2277 msgid "Edit Price Break" msgstr "" -#: company/views.py:456 part/views.py:2212 +#: company/views.py:456 part/views.py:2293 msgid "Delete Price Break" msgstr "" @@ -1431,7 +1431,7 @@ msgstr "" msgid "Date order was completed" msgstr "" -#: order/models.py:185 order/models.py:259 part/views.py:1304 +#: order/models.py:185 order/models.py:259 part/views.py:1343 #: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "" @@ -1600,7 +1600,7 @@ msgid "Purchase Order Attachments" msgstr "" #: order/templates/order/po_tabs.html:8 order/templates/order/so_tabs.html:16 -#: part/templates/part/tabs.html:64 stock/templates/stock/tabs.html:32 +#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 msgid "Attachments" msgstr "" @@ -1616,7 +1616,7 @@ msgstr "" #: order/templates/order/purchase_order_detail.html:38 #: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:161 part/templates/part/category.html:202 +#: part/templates/part/category.html:171 part/templates/part/category.html:213 #: templates/js/stock.html:803 msgid "New Location" msgstr "" @@ -1658,7 +1658,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:135 templates/js/part.html:388 +#: part/templates/part/part_base.html:145 templates/js/part.html:388 msgid "On Order" msgstr "" @@ -1750,7 +1750,7 @@ msgstr "" msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:102 order/views.py:149 part/views.py:86 stock/views.py:167 +#: order/views.py:102 order/views.py:149 part/views.py:90 stock/views.py:167 msgid "Added attachment" msgstr "" @@ -1890,12 +1890,12 @@ msgstr "" msgid "Remove allocation" msgstr "" -#: part/bom.py:138 part/templates/part/category.html:55 +#: part/bom.py:138 part/templates/part/category.html:61 #: part/templates/part/detail.html:87 msgid "Default Location" msgstr "" -#: part/bom.py:139 part/templates/part/part_base.html:108 +#: part/bom.py:139 part/templates/part/part_base.html:118 msgid "Available Stock" msgstr "" @@ -2013,7 +2013,7 @@ msgid "Part Category" msgstr "" #: part/models.py:76 part/templates/part/category.html:18 -#: part/templates/part/category.html:83 templates/stats.html:12 +#: part/templates/part/category.html:89 templates/stats.html:12 msgid "Part Categories" msgstr "" @@ -2226,7 +2226,7 @@ msgstr "" msgid "BOM line checksum" msgstr "" -#: part/models.py:1612 part/views.py:1310 part/views.py:1362 +#: part/models.py:1612 part/views.py:1349 part/views.py:1401 #: stock/models.py:231 msgid "Quantity must be integer value for trackable parts" msgstr "" @@ -2285,23 +2285,23 @@ msgstr "" msgid "Finish Editing" msgstr "" -#: part/templates/part/bom.html:42 +#: part/templates/part/bom.html:43 msgid "Edit BOM" msgstr "" -#: part/templates/part/bom.html:44 +#: part/templates/part/bom.html:45 msgid "Validate Bill of Materials" msgstr "" -#: part/templates/part/bom.html:46 part/views.py:1597 +#: part/templates/part/bom.html:48 part/views.py:1640 msgid "Export Bill of Materials" msgstr "" -#: part/templates/part/bom.html:101 +#: part/templates/part/bom.html:103 msgid "Delete selected BOM items?" msgstr "" -#: part/templates/part/bom.html:102 +#: part/templates/part/bom.html:104 msgid "All selected BOM items will be deleted" msgstr "" @@ -2373,83 +2373,91 @@ msgstr "" msgid "Each part must already exist in the database" msgstr "" +#: part/templates/part/build.html:8 +msgid "Part Builds" +msgstr "" + +#: part/templates/part/build.html:14 +msgid "Start New Build" +msgstr "" + #: part/templates/part/category.html:19 msgid "All parts" msgstr "" -#: part/templates/part/category.html:23 part/views.py:1976 +#: part/templates/part/category.html:24 part/views.py:2043 msgid "Create new part category" msgstr "" -#: part/templates/part/category.html:27 +#: part/templates/part/category.html:30 msgid "Edit part category" msgstr "" -#: part/templates/part/category.html:30 +#: part/templates/part/category.html:35 msgid "Delete part category" msgstr "" -#: part/templates/part/category.html:39 part/templates/part/category.html:78 +#: part/templates/part/category.html:45 part/templates/part/category.html:84 msgid "Category Details" msgstr "" -#: part/templates/part/category.html:44 +#: part/templates/part/category.html:50 msgid "Category Path" msgstr "" -#: part/templates/part/category.html:49 +#: part/templates/part/category.html:55 msgid "Category Description" msgstr "" -#: part/templates/part/category.html:62 part/templates/part/detail.html:64 +#: part/templates/part/category.html:68 part/templates/part/detail.html:64 msgid "Keywords" msgstr "" -#: part/templates/part/category.html:68 +#: part/templates/part/category.html:74 msgid "Subcategories" msgstr "" -#: part/templates/part/category.html:73 +#: part/templates/part/category.html:79 msgid "Parts (Including subcategories)" msgstr "" -#: part/templates/part/category.html:106 +#: part/templates/part/category.html:112 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:107 part/views.py:491 +#: part/templates/part/category.html:114 part/views.py:511 msgid "Create new part" msgstr "" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 msgid "Set category" msgstr "" -#: part/templates/part/category.html:111 +#: part/templates/part/category.html:120 msgid "Set Category" msgstr "" -#: part/templates/part/category.html:113 +#: part/templates/part/category.html:123 msgid "Export Data" msgstr "" -#: part/templates/part/category.html:162 +#: part/templates/part/category.html:172 msgid "Create new location" msgstr "" -#: part/templates/part/category.html:167 part/templates/part/category.html:196 +#: part/templates/part/category.html:177 part/templates/part/category.html:207 msgid "New Category" msgstr "" -#: part/templates/part/category.html:168 +#: part/templates/part/category.html:178 msgid "Create new category" msgstr "" -#: part/templates/part/category.html:197 +#: part/templates/part/category.html:208 msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:203 stock/views.py:1314 +#: part/templates/part/category.html:214 stock/views.py:1314 msgid "Create new Stock Location" msgstr "" @@ -2461,7 +2469,7 @@ msgstr "" msgid "Part Details" msgstr "" -#: part/templates/part/detail.html:25 part/templates/part/part_base.html:85 +#: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 #: templates/js/part.html:112 msgid "IPN" msgstr "" @@ -2491,7 +2499,7 @@ msgstr "" msgid "Default Supplier" msgstr "" -#: part/templates/part/detail.html:102 part/templates/part/params.html:22 +#: part/templates/part/detail.html:102 part/templates/part/params.html:24 msgid "Units" msgstr "" @@ -2616,24 +2624,25 @@ msgstr "" msgid "Part Parameters" msgstr "" -#: part/templates/part/params.html:13 +#: part/templates/part/params.html:14 msgid "Add new parameter" msgstr "" -#: part/templates/part/params.html:13 templates/InvenTree/settings/part.html:12 +#: part/templates/part/params.html:14 templates/InvenTree/settings/part.html:12 msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:21 stock/models.py:1391 +#: part/templates/part/params.html:23 stock/models.py:1391 #: templates/js/stock.html:112 msgid "Value" msgstr "" -#: part/templates/part/params.html:33 +#: part/templates/part/params.html:36 msgid "Edit" msgstr "" -#: part/templates/part/params.html:34 part/templates/part/supplier.html:17 +#: part/templates/part/params.html:39 part/templates/part/supplier.html:17 +#: users/models.py:141 msgid "Delete" msgstr "" @@ -2684,39 +2693,43 @@ msgstr "" msgid "Show pricing information" msgstr "" -#: part/templates/part/part_base.html:70 -msgid "Part actions" -msgstr "" - -#: part/templates/part/part_base.html:72 -msgid "Duplicate part" -msgstr "" - -#: part/templates/part/part_base.html:73 -msgid "Edit part" +#: part/templates/part/part_base.html:60 +msgid "Count part stock" msgstr "" #: part/templates/part/part_base.html:75 +msgid "Part actions" +msgstr "" + +#: part/templates/part/part_base.html:78 +msgid "Duplicate part" +msgstr "" + +#: part/templates/part/part_base.html:81 +msgid "Edit part" +msgstr "" + +#: part/templates/part/part_base.html:84 msgid "Delete part" msgstr "" -#: part/templates/part/part_base.html:114 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:124 templates/js/table_filters.html:65 msgid "In Stock" msgstr "" -#: part/templates/part/part_base.html:121 +#: part/templates/part/part_base.html:131 msgid "Allocated to Build Orders" msgstr "" -#: part/templates/part/part_base.html:128 +#: part/templates/part/part_base.html:138 msgid "Allocated to Sales Orders" msgstr "" -#: part/templates/part/part_base.html:150 +#: part/templates/part/part_base.html:160 msgid "Can Build" msgstr "" -#: part/templates/part/part_base.html:156 +#: part/templates/part/part_base.html:166 msgid "Underway" msgstr "" @@ -2736,7 +2749,7 @@ msgstr "" msgid "Upload new image" msgstr "" -#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:50 +#: part/templates/part/sale_prices.html:9 part/templates/part/tabs.html:53 msgid "Sale Price" msgstr "" @@ -2797,11 +2810,11 @@ msgstr "" msgid "BOM" msgstr "" -#: part/templates/part/tabs.html:34 +#: part/templates/part/tabs.html:37 msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:58 stock/templates/stock/item_base.html:282 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:282 msgid "Tests" msgstr "" @@ -2829,176 +2842,176 @@ msgstr "" msgid "New Variant" msgstr "" -#: part/views.py:76 +#: part/views.py:78 msgid "Add part attachment" msgstr "" -#: part/views.py:125 templates/attachment_table.html:30 +#: part/views.py:129 templates/attachment_table.html:30 msgid "Edit attachment" msgstr "" -#: part/views.py:129 +#: part/views.py:135 msgid "Part attachment updated" msgstr "" -#: part/views.py:144 +#: part/views.py:150 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:150 +#: part/views.py:158 msgid "Deleted part attachment" msgstr "" -#: part/views.py:159 +#: part/views.py:167 msgid "Create Test Template" msgstr "" -#: part/views.py:186 +#: part/views.py:196 msgid "Edit Test Template" msgstr "" -#: part/views.py:200 +#: part/views.py:212 msgid "Delete Test Template" msgstr "" -#: part/views.py:207 +#: part/views.py:221 msgid "Set Part Category" msgstr "" -#: part/views.py:255 +#: part/views.py:271 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:290 +#: part/views.py:306 msgid "Create Variant" msgstr "" -#: part/views.py:368 +#: part/views.py:386 msgid "Duplicate Part" msgstr "" -#: part/views.py:373 +#: part/views.py:393 msgid "Copied part" msgstr "" -#: part/views.py:496 +#: part/views.py:518 msgid "Created new part" msgstr "" -#: part/views.py:707 +#: part/views.py:733 msgid "Part QR Code" msgstr "" -#: part/views.py:724 +#: part/views.py:752 msgid "Upload Part Image" msgstr "" -#: part/views.py:729 part/views.py:764 +#: part/views.py:760 part/views.py:797 msgid "Updated part image" msgstr "" -#: part/views.py:738 +#: part/views.py:769 msgid "Select Part Image" msgstr "" -#: part/views.py:767 +#: part/views.py:800 msgid "Part image not found" msgstr "" -#: part/views.py:778 +#: part/views.py:811 msgid "Edit Part Properties" msgstr "" -#: part/views.py:800 +#: part/views.py:835 msgid "Validate BOM" msgstr "" -#: part/views.py:963 +#: part/views.py:1002 msgid "No BOM file provided" msgstr "" -#: part/views.py:1313 +#: part/views.py:1352 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1338 part/views.py:1341 +#: part/views.py:1377 part/views.py:1380 msgid "Select valid part" msgstr "" -#: part/views.py:1347 +#: part/views.py:1386 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1385 +#: part/views.py:1424 msgid "Select a part" msgstr "" -#: part/views.py:1391 +#: part/views.py:1430 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1395 +#: part/views.py:1434 msgid "Specify quantity" msgstr "" -#: part/views.py:1645 +#: part/views.py:1690 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1652 +#: part/views.py:1699 msgid "Part was deleted" msgstr "" -#: part/views.py:1661 +#: part/views.py:1708 msgid "Part Pricing" msgstr "" -#: part/views.py:1783 +#: part/views.py:1834 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1791 +#: part/views.py:1844 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1798 +#: part/views.py:1853 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1806 +#: part/views.py:1863 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1856 +#: part/views.py:1915 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:1870 +#: part/views.py:1931 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1927 +#: part/views.py:1990 msgid "Edit Part Category" msgstr "" -#: part/views.py:1962 +#: part/views.py:2027 msgid "Delete Part Category" msgstr "" -#: part/views.py:1968 +#: part/views.py:2035 msgid "Part category was deleted" msgstr "" -#: part/views.py:2027 +#: part/views.py:2098 msgid "Create BOM item" msgstr "" -#: part/views.py:2093 +#: part/views.py:2166 msgid "Edit BOM item" msgstr "" -#: part/views.py:2141 +#: part/views.py:2216 msgid "Confim BOM item deletion" msgstr "" @@ -3371,15 +3384,15 @@ msgid "Stock adjustment actions" msgstr "" #: stock/templates/stock/item_base.html:98 -#: stock/templates/stock/location.html:38 templates/stock_table.html:15 +#: stock/templates/stock/location.html:38 templates/stock_table.html:19 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:99 templates/stock_table.html:13 +#: stock/templates/stock/item_base.html:99 templates/stock_table.html:17 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:100 templates/stock_table.html:14 +#: stock/templates/stock/item_base.html:100 templates/stock_table.html:18 msgid "Remove stock" msgstr "" @@ -3819,6 +3832,14 @@ msgstr "" msgid "Add Stock Tracking Entry" msgstr "" +#: templates/403.html:5 templates/403.html:11 +msgid "Permission Denied" +msgstr "" + +#: templates/403.html:14 +msgid "You do not have permission to view this page." +msgstr "" + #: templates/InvenTree/bom_invalid.html:7 msgid "BOM Waiting Validation" msgstr "" @@ -3827,6 +3848,10 @@ msgstr "" msgid "Pending Builds" msgstr "" +#: templates/InvenTree/index.html:4 +msgid "Index" +msgstr "" + #: templates/InvenTree/latest_parts.html:7 msgid "Latest Parts" msgstr "" @@ -4392,39 +4417,39 @@ msgstr "" msgid "Purchasable" msgstr "" -#: templates/navbar.html:22 +#: templates/navbar.html:29 msgid "Buy" msgstr "" -#: templates/navbar.html:30 +#: templates/navbar.html:39 msgid "Sell" msgstr "" -#: templates/navbar.html:40 +#: templates/navbar.html:50 msgid "Scan Barcode" msgstr "" -#: templates/navbar.html:49 +#: templates/navbar.html:59 users/models.py:27 msgid "Admin" msgstr "" -#: templates/navbar.html:52 +#: templates/navbar.html:62 msgid "Settings" msgstr "" -#: templates/navbar.html:53 +#: templates/navbar.html:63 msgid "Logout" msgstr "" -#: templates/navbar.html:55 +#: templates/navbar.html:65 msgid "Login" msgstr "" -#: templates/navbar.html:58 +#: templates/navbar.html:68 msgid "About InvenTree" msgstr "" -#: templates/navbar.html:59 +#: templates/navbar.html:69 msgid "Statistics" msgstr "" @@ -4436,38 +4461,94 @@ msgstr "" msgid "Export Stock Information" msgstr "" -#: templates/stock_table.html:13 +#: templates/stock_table.html:17 msgid "Add to selected stock items" msgstr "" -#: templates/stock_table.html:14 +#: templates/stock_table.html:18 msgid "Remove from selected stock items" msgstr "" -#: templates/stock_table.html:15 +#: templates/stock_table.html:19 msgid "Stocktake selected stock items" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 msgid "Move selected stock items" msgstr "" -#: templates/stock_table.html:16 +#: templates/stock_table.html:20 msgid "Move stock" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Order selected items" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Order stock" msgstr "" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 msgid "Delete selected items" msgstr "" -#: templates/stock_table.html:18 +#: templates/stock_table.html:24 msgid "Delete Stock" msgstr "" + +#: users/admin.py:62 +msgid "Users" +msgstr "" + +#: users/admin.py:63 +msgid "Select which users are assigned to this group" +msgstr "" + +#: users/admin.py:124 +msgid "Personal info" +msgstr "" + +#: users/admin.py:125 +msgid "Permissions" +msgstr "" + +#: users/admin.py:128 +msgid "Important dates" +msgstr "" + +#: users/models.py:124 +msgid "Permission set" +msgstr "" + +#: users/models.py:132 +msgid "Group" +msgstr "" + +#: users/models.py:135 +msgid "View" +msgstr "" + +#: users/models.py:135 +msgid "Permission to view items" +msgstr "" + +#: users/models.py:137 +msgid "Create" +msgstr "" + +#: users/models.py:137 +msgid "Permission to add items" +msgstr "" + +#: users/models.py:139 +msgid "Update" +msgstr "" + +#: users/models.py:139 +msgid "Permissions to edit items" +msgstr "" + +#: users/models.py:141 +msgid "Permission to delete items" +msgstr "" From 16d720b62c08b5e22289d019f4ea27d949e26bca Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 00:36:55 +1100 Subject: [PATCH 57/77] Update permission requirements for API - Automatically use model permissions by default! - --- InvenTree/InvenTree/settings.py | 4 ++++ InvenTree/build/api.py | 14 +----------- InvenTree/company/api.py | 19 +---------------- InvenTree/order/api.py | 28 +----------------------- InvenTree/part/api.py | 38 --------------------------------- InvenTree/stock/api.py | 15 ------------- 6 files changed, 7 insertions(+), 111 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 21b8a0ead1..75f6f58993 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -231,6 +231,10 @@ REST_FRAMEWORK = { 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + 'rest_framework.permissions.DjangoModelPermissions', + ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' } diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index c0faee6c15..d4e458c506 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters -from rest_framework import generics, permissions +from rest_framework import generics from django.conf.urls import url, include @@ -28,10 +28,6 @@ class BuildList(generics.ListCreateAPIView): queryset = Build.objects.all() serializer_class = BuildSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -99,10 +95,6 @@ class BuildDetail(generics.RetrieveUpdateAPIView): queryset = Build.objects.all() serializer_class = BuildSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - class BuildItemList(generics.ListCreateAPIView): """ API endpoint for accessing a list of BuildItem objects @@ -137,10 +129,6 @@ class BuildItemList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, ] diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 0e54d7d7fb..548ac96016 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters -from rest_framework import generics, permissions +from rest_framework import generics from django.conf.urls import url, include from django.db.models import Q @@ -40,10 +40,6 @@ class CompanyList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -82,10 +78,6 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - class SupplierPartList(generics.ListCreateAPIView): """ API endpoint for list view of SupplierPart object @@ -170,10 +162,6 @@ class SupplierPartList(generics.ListCreateAPIView): serializer_class = SupplierPartSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -202,7 +190,6 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView): queryset = SupplierPart.objects.all() serializer_class = SupplierPartSerializer - permission_classes = (permissions.IsAuthenticated,) read_only_fields = [ ] @@ -218,10 +205,6 @@ class SupplierPriceBreakList(generics.ListCreateAPIView): queryset = SupplierPriceBreak.objects.all() serializer_class = SupplierPriceBreakSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, ] diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index a7915878c5..4a9dbfa2ac 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -6,7 +6,7 @@ JSON API for the Order app from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import generics, permissions +from rest_framework import generics from rest_framework import filters from django.conf.urls import url, include @@ -109,10 +109,6 @@ class POList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -162,10 +158,6 @@ class PODetail(generics.RetrieveUpdateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated - ] - class POLineItemList(generics.ListCreateAPIView): """ API endpoint for accessing a list of POLineItem objects @@ -188,10 +180,6 @@ class POLineItemList(generics.ListCreateAPIView): return self.serializer_class(*args, **kwargs) - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, ] @@ -208,10 +196,6 @@ class POLineItemDetail(generics.RetrieveUpdateAPIView): queryset = PurchaseOrderLineItem serializer_class = POLineItemSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ @@ -300,10 +284,6 @@ class SOList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -351,8 +331,6 @@ class SODetail(generics.RetrieveUpdateAPIView): return queryset - permission_classes = [permissions.IsAuthenticated] - class SOLineItemList(generics.ListCreateAPIView): """ @@ -398,8 +376,6 @@ class SOLineItemList(generics.ListCreateAPIView): return queryset - permission_classes = [permissions.IsAuthenticated] - filter_backends = [DjangoFilterBackend] filter_fields = [ @@ -414,8 +390,6 @@ class SOLineItemDetail(generics.RetrieveUpdateAPIView): queryset = SalesOrderLineItem.objects.all() serializer_class = SOLineItemSerializer - permission_classes = [permissions.IsAuthenticated] - class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 6834503466..d4aeec5bd9 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -55,10 +55,6 @@ class CategoryList(generics.ListCreateAPIView): queryset = PartCategory.objects.all() serializer_class = part_serializers.CategorySerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - def get_queryset(self): """ Custom filtering: @@ -119,10 +115,6 @@ class PartSalePriceList(generics.ListCreateAPIView): queryset = PartSellPriceBreak.objects.all() serializer_class = part_serializers.PartSalePriceSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend ] @@ -182,8 +174,6 @@ class PartTestTemplateList(generics.ListCreateAPIView): return queryset - permission_classes = [permissions.IsAuthenticated] - filter_backends = [ DjangoFilterBackend, filters.OrderingFilter, @@ -221,10 +211,6 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView): queryset = Part.objects.all() serializer_class = part_serializers.PartThumbSerializerUpdate - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend ] @@ -246,10 +232,6 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - def get_serializer(self, *args, **kwargs): try: @@ -580,10 +562,6 @@ class PartList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -676,10 +654,6 @@ class PartParameterTemplateList(generics.ListCreateAPIView): queryset = PartParameterTemplate.objects.all() serializer_class = part_serializers.PartParameterTemplateSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ filters.OrderingFilter, ] @@ -699,10 +673,6 @@ class PartParameterList(generics.ListCreateAPIView): queryset = PartParameter.objects.all() serializer_class = part_serializers.PartParameterSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend ] @@ -796,10 +766,6 @@ class BomList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -816,10 +782,6 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView): queryset = BomItem.objects.all() serializer_class = part_serializers.BomItemSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - class BomItemValidate(generics.UpdateAPIView): """ API endpoint for validating a BomItem """ diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 55bc62a44e..790de7d879 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -68,7 +68,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): queryset = StockItem.objects.all() serializer_class = StockItemSerializer - permission_classes = (permissions.IsAuthenticated,) def get_queryset(self, *args, **kwargs): @@ -289,10 +288,6 @@ class StockLocationList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -695,10 +690,6 @@ class StockList(generics.ListCreateAPIView): return queryset - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -744,10 +735,6 @@ class StockItemTestResultList(generics.ListCreateAPIView): queryset = StockItemTestResult.objects.all() serializer_class = StockItemTestResultSerializer - permission_classes = [ - permissions.IsAuthenticated, - ] - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, @@ -799,7 +786,6 @@ class StockTrackingList(generics.ListCreateAPIView): queryset = StockItemTracking.objects.all() serializer_class = StockTrackingSerializer - permission_classes = [permissions.IsAuthenticated] def get_serializer(self, *args, **kwargs): try: @@ -871,7 +857,6 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView): queryset = StockLocation.objects.all() serializer_class = LocationSerializer - permission_classes = (permissions.IsAuthenticated,) stock_endpoints = [ From 3f59ce3f93030c37afe37e2f69a547f927ddd70d Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 01:30:36 +1100 Subject: [PATCH 58/77] Update unit tests - requires the user to actually have the necessary permissions! --- InvenTree/InvenTree/api.py | 2 ++ InvenTree/InvenTree/helpers.py | 20 +++++++++++++++++++ InvenTree/company/test_api.py | 13 ++++++++++++- InvenTree/part/test_api.py | 29 +++++++++++++++++++++++++++- InvenTree/part/test_views.py | 35 ++++++++++++++++++++++++++++++++-- InvenTree/part/views.py | 12 ++++++------ InvenTree/stock/test_api.py | 16 ++++++++++++++++ 7 files changed, 117 insertions(+), 10 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 44e7ec383f..1bdfb79ceb 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -30,6 +30,8 @@ class InfoView(AjaxView): Use to confirm that the server is running, etc. """ + permission_classes = [permissions.AllowAny] + def get(self, request, *args, **kwargs): data = { diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 9b470902b1..13b770539c 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -15,6 +15,8 @@ from django.http import StreamingHttpResponse from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ +from django.contrib.auth.models import Permission + import InvenTree.version from .settings import MEDIA_URL, STATIC_URL @@ -441,3 +443,21 @@ def validateFilterString(value): results[k] = v return results + + +def addUserPermission(user, permission): + """ + Shortcut function for adding a certain permission to a user. + """ + + perm = Permission.objects.get(codename=permission) + user.user_permissions.add(perm) + + +def addUserPermissions(user, permissions): + """ + Shortcut function for adding multiple permissions to a user. + """ + + for permission in permissions: + addUserPermission(user, permission) diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py index bf4cc6643e..643608542d 100644 --- a/InvenTree/company/test_api.py +++ b/InvenTree/company/test_api.py @@ -3,6 +3,8 @@ from rest_framework import status from django.urls import reverse from django.contrib.auth import get_user_model +from InvenTree.helpers import addUserPermissions + from .models import Company @@ -14,7 +16,16 @@ class CompanyTest(APITestCase): def setUp(self): # Create a user for auth User = get_user_model() - User.objects.create_user('testuser', 'test@testing.com', 'password') + self.user = User.objects.create_user('testuser', 'test@testing.com', 'password') + + perms = [ + 'view_company', + 'change_company', + 'add_company', + ] + + addUserPermissions(self.user, perms) + self.client.login(username='testuser', password='password') Company.objects.create(name='ACME', description='Supplier', is_customer=False, is_supplier=True) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 3b116fa445..9fdc2688cb 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -9,6 +9,7 @@ from stock.models import StockItem from company.models import Company from InvenTree.status_codes import StockStatus +from InvenTree.helpers import addUserPermissions class PartAPITest(APITestCase): @@ -29,7 +30,33 @@ class PartAPITest(APITestCase): def setUp(self): # Create a user for auth User = get_user_model() - User.objects.create_user('testuser', 'test@testing.com', 'password') + self.user = User.objects.create_user( + username='testuser', + email='test@testing.com', + password='password' + ) + + # Add the permissions required to access the API endpoints + perms = [ + 'view_part', + 'add_part', + 'change_part', + 'delete_part', + 'view_partcategory', + 'add_partcategory', + 'change_partcategory', + 'view_bomitem', + 'add_bomitem', + 'change_bomitem', + 'view_partattachment', + 'change_partattachment', + 'add_partattachment', + 'view_parttesttemplate', + 'add_parttesttemplate', + 'change_parttesttemplate', + ] + + addUserPermissions(self.user, perms) self.client.login(username='testuser', password='password') diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index bc09784a47..b1ae991a0c 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -4,6 +4,8 @@ from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model +from InvenTree.helpers import addUserPermissions + from .models import Part @@ -23,7 +25,32 @@ class PartViewTestCase(TestCase): # Create a user User = get_user_model() - User.objects.create_user('username', 'user@email.com', 'password') + self.user = User.objects.create_user( + username='username', + email='user@email.com', + password='password' + ) + + # Add the permissions required to access the pages + perms = [ + 'view_part', + 'add_part', + 'change_part', + 'delete_part', + 'view_partcategory', + 'add_partcategory', + 'change_partcategory', + 'view_bomitem', + 'add_bomitem', + 'change_bomitem', + 'view_partattachment', + 'change_partattachment', + 'add_partattachment', + ] + + addUserPermissions(self.user, perms) + + self.user.save() self.client.login(username='username', password='password') @@ -140,12 +167,14 @@ class PartTests(PartViewTestCase): """ Tests for Part forms """ def test_part_edit(self): + response = self.client.get(reverse('part-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) keys = response.context.keys() data = str(response.content) + self.assertEqual(response.status_code, 200) + self.assertIn('part', keys) self.assertIn('csrf_token', keys) @@ -189,6 +218,8 @@ class PartAttachmentTests(PartViewTestCase): response = self.client.get(reverse('part-attachment-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) + # TODO - Create a new attachment using this view + def test_invalid_create(self): """ test creation of an attachment for an invalid part """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 47a77078be..8e0980c35e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -635,7 +635,7 @@ class PartNotes(UpdateView): template_name = 'part/notes.html' model = Part - permission_required = 'part.update_part' + permission_required = 'part.change_part' fields = ['notes'] @@ -753,7 +753,7 @@ class PartImageUpload(AjaxUpdateView): form_class = part_forms.PartImageForm - permission_required = 'part.update_part' + permission_required = 'part.change_part' def get_data(self): return { @@ -768,7 +768,7 @@ class PartImageSelect(AjaxUpdateView): ajax_template_name = 'part/select_image.html' ajax_form_title = _('Select Part Image') - permission_required = 'part.update_part' + permission_required = 'part.change_part' fields = [ 'image', @@ -811,7 +811,7 @@ class PartEdit(AjaxUpdateView): ajax_form_title = _('Edit Part Properties') context_object_name = 'part' - permission_required = 'part.update_part' + permission_required = 'part.change_part' def get_form(self): """ Create form for Part editing. @@ -837,7 +837,7 @@ class BomValidate(AjaxUpdateView): context_object_name = 'part' form_class = part_forms.BomValidateForm - permission_required = ('part.update_part') + permission_required = ('part.change_part') def get_context(self): return { @@ -905,7 +905,7 @@ class BomUpload(PermissionRequiredMixin, FormView): missing_columns = [] allowed_parts = [] - permission_required = ('part.update_part', 'part.add_bomitem') + permission_required = ('part.change_part', 'part.add_bomitem') def get_success_url(self): part = self.get_object() diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index a522bc5415..8348a3e331 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -3,6 +3,8 @@ from rest_framework import status from django.urls import reverse from django.contrib.auth import get_user_model +from InvenTree.helpers import addUserPermissions + from .models import StockLocation @@ -22,6 +24,20 @@ class StockAPITestCase(APITestCase): # Create a user for auth User = get_user_model() self.user = User.objects.create_user('testuser', 'test@testing.com', 'password') + + # Add the necessary permissions to the user + perms = [ + 'view_stockitemtestresult', + 'change_stockitemtestresult', + 'add_stockitemtestresult', + 'add_stocklocation', + 'change_stocklocation', + 'add_stockitem', + 'change_stockitem', + ] + + addUserPermissions(self.user, perms) + self.client.login(username='testuser', password='password') def doPost(self, url, data={}): From c910307ce5aeabf6f79eba77c661e819fffc1bfc Mon Sep 17 00:00:00 2001 From: eeintech <eeintech@eeinte.ch> Date: Mon, 5 Oct 2020 10:04:54 -0500 Subject: [PATCH 59/77] Only saving Group model rulesets on instance creation and when inlines are saved --- InvenTree/users/admin.py | 8 ++------ InvenTree/users/models.py | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index 86d4bb1a86..765bc0d795 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -97,15 +97,11 @@ class RoleGroupAdmin(admin.ModelAdmin): # Save inlines before model # https://stackoverflow.com/a/14860703/12794913 def save_model(self, request, obj, form, change): - if obj is not None: - # Save model immediately only if in 'Add role' view - super().save_model(request, obj, form, change) - else: - pass # don't actually save the parent instance + pass # don't actually save the parent instance def save_formset(self, request, form, formset, change): formset.save() # this will save the children - form.instance.save() # form.instance is the parent + form.instance.save(update_fields=['name']) # form.instance is the parent class InvenTreeUserAdmin(UserAdmin): diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 5fe86e15fa..990d3770c0 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -155,8 +155,15 @@ class RuleSet(models.Model): model=model ) - def __str__(self): - return self.name + def __str__(self, debug=False): + """ Ruleset string representation """ + if debug: + # Makes debugging easier + return f'{str(self.group).ljust(15)}: {self.name.title().ljust(15)} | ' \ + f'v: {str(self.can_view).ljust(5)} | a: {str(self.can_add).ljust(5)} | ' \ + f'c: {str(self.can_change).ljust(5)} | d: {str(self.can_delete).ljust(5)}' + else: + return self.name def save(self, *args, **kwargs): @@ -327,5 +334,8 @@ def create_missing_rule_sets(sender, instance, **kwargs): then we can now use these RuleSet values to update the group permissions. """ + created = kwargs.get('created', False) + update_fields = kwargs.get('update_fields', None) - update_group_roles(instance) + if created or update_fields: + update_group_roles(instance) From d980da72472a430219988be89fabcfd50993535f Mon Sep 17 00:00:00 2001 From: eeintech <eeintech@eeinte.ch> Date: Mon, 5 Oct 2020 10:52:47 -0500 Subject: [PATCH 60/77] Fixed permission assign test unit --- InvenTree/users/admin.py | 1 + InvenTree/users/models.py | 1 + InvenTree/users/tests.py | 6 ++++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index 765bc0d795..e4974a3f7a 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -101,6 +101,7 @@ class RoleGroupAdmin(admin.ModelAdmin): def save_formset(self, request, form, formset, change): formset.save() # this will save the children + # update_fields is required to trigger permissions update form.instance.save(update_fields=['name']) # form.instance is the parent diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 990d3770c0..df8e91d293 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -335,6 +335,7 @@ def create_missing_rule_sets(sender, instance, **kwargs): group permissions. """ created = kwargs.get('created', False) + # To trigger the group permissions update: update_fields should not be None update_fields = kwargs.get('update_fields', None) if created or update_fields: diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index d14ffc4950..e277422f71 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -137,7 +137,8 @@ class RuleSetModelTest(TestCase): rule.save() - group.save() + # update_fields is required to trigger permissions update + group.save(update_fields=['name']) # There should now be three permissions for each rule set self.assertEqual(group.permissions.count(), 3 * len(permission_set)) @@ -151,7 +152,8 @@ class RuleSetModelTest(TestCase): rule.save() - group.save() + # update_fields is required to trigger permissions update + group.save(update_fields=['name']) # There should now not be any permissions assigned to this group self.assertEqual(group.permissions.count(), 0) From 8b2189dacad374b39478a1452a9a72d186a1b840 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 09:27:22 +1100 Subject: [PATCH 61/77] Add global context 'roles' - Access via template e.g. {% if roles.part.view %} - Always True if the user is a superuser --- InvenTree/InvenTree/context.py | 33 +++++++++++++++++++++++++++++++++ InvenTree/InvenTree/settings.py | 1 + 2 files changed, 34 insertions(+) diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index 7de41eef15..71aee5c2c5 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -17,3 +17,36 @@ def status_codes(request): 'BuildStatus': BuildStatus, 'StockStatus': StockStatus, } + + +def user_roles(request): + """ + Return a map of the current roles assigned to the user. + + Roles are denoted by their simple names, and then the permission type. + + Permissions can be access as follows: + + - roles.part.view + - roles.build.delete + + Each value will return a boolean True / False + """ + + user = request.user + + roles = {} + + for group in user.groups.all(): + for rule in group.rule_sets.all(): + roles[rule.name] = { + 'view': rule.can_view or user.is_superuser, + 'add': rule.can_add or user.is_superuser, + 'change': rule.can_change or user.is_superuser, + 'delete': rule.can_delete or user.is_superuser, + } + + print("Roles:") + print(roles) + + return {'roles': roles} diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 75f6f58993..c6f8b40069 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -210,6 +210,7 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'InvenTree.context.status_codes', + 'InvenTree.context.user_roles', ], }, }, From 556ffa1099197a0f41d734dbdcc4782e2e40b5ee Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 09:28:05 +1100 Subject: [PATCH 62/77] Change label for permissions to match django permission names --- .../migrations/0003_auto_20201005_2227.py | 23 +++++++++++++++++++ InvenTree/users/models.py | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 InvenTree/users/migrations/0003_auto_20201005_2227.py diff --git a/InvenTree/users/migrations/0003_auto_20201005_2227.py b/InvenTree/users/migrations/0003_auto_20201005_2227.py new file mode 100644 index 0000000000..92d7e341fa --- /dev/null +++ b/InvenTree/users/migrations/0003_auto_20201005_2227.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.7 on 2020-10-05 22:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_auto_20201004_0158'), + ] + + operations = [ + migrations.AlterField( + model_name='ruleset', + name='can_add', + field=models.BooleanField(default=False, help_text='Permission to add items', verbose_name='Add'), + ), + migrations.AlterField( + model_name='ruleset', + name='can_change', + field=models.BooleanField(default=False, help_text='Permissions to edit items', verbose_name='Change'), + ), + ] diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 5fe86e15fa..3bd976f0a4 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -134,9 +134,9 @@ class RuleSet(models.Model): can_view = models.BooleanField(verbose_name=_('View'), default=True, help_text=_('Permission to view items')) - can_add = models.BooleanField(verbose_name=_('Create'), default=False, help_text=_('Permission to add items')) + can_add = models.BooleanField(verbose_name=_('Add'), default=False, help_text=_('Permission to add items')) - can_change = models.BooleanField(verbose_name=_('Update'), default=False, help_text=_('Permissions to edit items')) + can_change = models.BooleanField(verbose_name=_('Change'), default=False, help_text=_('Permissions to edit items')) can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items')) From 23aee234f0002b35f93945994562ccb3037d5d92 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 09:32:05 +1100 Subject: [PATCH 63/77] Change index page to use roles rather than perms to determine user permissions --- InvenTree/templates/InvenTree/index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index b20d61116d..8e59d51d2b 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -9,24 +9,24 @@ InvenTree | {% trans "Index" %} <hr> <div class='col-sm-6'> - {% if perms.part.view_part %} + {% if roles.part.view %} {% include "InvenTree/latest_parts.html" with collapse_id="latest_parts" %} {% include "InvenTree/bom_invalid.html" with collapse_id="bom_invalid" %} {% include "InvenTree/starred_parts.html" with collapse_id="starred" %} {% endif %} - {% if perms.build.view_build %} + {% if roles.build.view %} {% include "InvenTree/build_pending.html" with collapse_id="build_pending" %} {% endif %} </div> <div class='col-sm-6'> - {% if perms.stock.view_stockitem %} + {% if roles.stock.view %} {% include "InvenTree/low_stock.html" with collapse_id="order" %} {% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %} {% endif %} - {% if perms.order.view_purchaseorder %} + {% if roles.purchase_order.view %} {% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %} {% endif %} - {% if perms.order.view_salesorder %} + {% if roles.sales_order.view %} {% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %} {% endif %} </div> From fa21d66c411aadf71031ba415b02ea12e64fd3cf Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 09:54:37 +1100 Subject: [PATCH 64/77] Fix logic for global context object 'roles' - User may be a part of multiple groups - Roles are additive across groups --- InvenTree/InvenTree/context.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index 71aee5c2c5..f9d856f566 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -35,18 +35,25 @@ def user_roles(request): user = request.user - roles = {} + roles = { + } for group in user.groups.all(): for rule in group.rule_sets.all(): - roles[rule.name] = { - 'view': rule.can_view or user.is_superuser, - 'add': rule.can_add or user.is_superuser, - 'change': rule.can_change or user.is_superuser, - 'delete': rule.can_delete or user.is_superuser, - } - print("Roles:") - print(roles) + # Ensure the role name is in the dict + if rule.name not in roles: + roles[rule.name] = { + 'view': user.is_superuser, + 'add': user.is_superuser, + 'change': user.is_superuser, + 'delete': user.is_superuser + } + + # Roles are additive across groups + roles[rule.name]['view'] |= rule.can_view + roles[rule.name]['add'] |= rule.can_add + roles[rule.name]['change'] |= rule.can_change + roles[rule.name]['delete'] |= rule.can_delete return {'roles': roles} From d2e2e7511f70e90c2e6f3787bc66bc5ea65a9df3 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 10:07:39 +1100 Subject: [PATCH 65/77] Update templates: Change perms to roles --- InvenTree/part/templates/part/bom.html | 2 +- InvenTree/part/templates/part/build.html | 2 +- InvenTree/part/templates/part/category.html | 14 +++++++------- InvenTree/part/templates/part/notes.html | 2 +- InvenTree/part/templates/part/params.html | 8 ++++---- InvenTree/part/templates/part/part_base.html | 16 ++++++++-------- InvenTree/part/templates/part/tabs.html | 2 +- InvenTree/templates/navbar.html | 10 +++++----- InvenTree/templates/stock_table.html | 8 ++++---- 9 files changed, 32 insertions(+), 32 deletions(-) diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 610e60847a..c996edc2db 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -39,7 +39,7 @@ <button class='btn btn-default action-button' type='button' title='{% trans "New BOM Item" %}' id='bom-item-new'><span class='fas fa-plus-circle'></span></button> <button class='btn btn-default action-button' type='button' title='{% trans "Finish Editing" %}' id='editing-finished'><span class='fas fa-check-circle'></span></button> {% elif part.active %} - {% if perms.part.change_part %} + {% if roles.part.change %} <button class='btn btn-default action-button' type='button' title='{% trans "Edit BOM" %}' id='edit-bom'><span class='fas fa-edit'></span></button> {% if part.is_bom_valid == False %} <button class='btn btn-default action-button' id='validate-bom' title='{% trans "Validate Bill of Materials" %}' type='button'><span class='fas fa-clipboard-check'></span></button> diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index ad51d33ab2..bfd72a2f70 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -10,7 +10,7 @@ <div id='button-toolbar'> <div class='button-toolbar container-flui' style='float: right';> {% if part.active %} - {% if perms.build.add_build %} + {% if roles.build.add %} <button class="btn btn-success" id='start-build'>{% trans "Start New Build" %}</button> {% endif %} {% endif %} diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 25417a1d9b..d73aba0291 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -9,7 +9,7 @@ {% if category %} <h3> {{ category.name }} - {% if user.is_staff and perms.part.change_partcategory %} + {% if user.is_staff and roles.part.change %} <a href="{% url 'admin:part_partcategory_change' category.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a> {% endif %} </h3> @@ -20,18 +20,18 @@ {% endif %} <p> <div class='btn-group action-buttons'> - {% if perms.part.add_partcategory %} + {% if roles.part.add %} <button class='btn btn-default' id='cat-create' title='{% trans "Create new part category" %}'> <span class='fas fa-plus-circle icon-green'/> </button> {% endif %} {% if category %} - {% if perms.part.change_partcategory %} + {% if roles.part.change %} <button class='btn btn-default' id='cat-edit' title='{% trans "Edit part category" %}'> <span class='fas fa-edit icon-blue'/> </button> {% endif %} - {% if perms.part.delete_partcategory %} + {% if roles.part.delete %} <button class='btn btn-default' id='cat-delete' title='{% trans "Delete part category" %}'> <span class='fas fa-trash-alt icon-red'/> </button> @@ -110,13 +110,13 @@ <div class='button-toolbar container-fluid' style="float: right;"> <div class='btn-group'> <button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>{% trans "Export" %}</button> - {% if perms.part.add_part %} + {% if roles.part.add %} <button class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>{% trans "New Part" %}</button> {% endif %} <div class='btn-group'> <button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown">{% trans "Options" %}<span class='caret'></span></button> <ul class='dropdown-menu'> - {% if perms.part.change_part %} + {% if roles.part.change %} <li><a href='#' id='multi-part-category' title='{% trans "Set category" %}'>{% trans "Set Category" %}</a></li> {% endif %} <li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li> @@ -190,7 +190,7 @@ location.href = url; }); - {% if perms.part.add_part %} + {% if roles.part.add %} $("#part-create").click(function() { launchModalForm( "{% url 'part-create' %}", diff --git a/InvenTree/part/templates/part/notes.html b/InvenTree/part/templates/part/notes.html index 68711c881e..3f833325cd 100644 --- a/InvenTree/part/templates/part/notes.html +++ b/InvenTree/part/templates/part/notes.html @@ -29,7 +29,7 @@ <h4>{% trans "Part Notes" %}</h4> </div> <div class='col-sm-6'> - {% if perms.part.change_part %} + {% if roles.part.change %} <button title='{% trans "Edit notes" %}' class='btn btn-default action-button float-right' id='edit-notes'><span class='fas fa-edit'></span></button> {% endif %} </div> diff --git a/InvenTree/part/templates/part/params.html b/InvenTree/part/templates/part/params.html index 9984830242..ba8aa0566d 100644 --- a/InvenTree/part/templates/part/params.html +++ b/InvenTree/part/templates/part/params.html @@ -10,7 +10,7 @@ <div id='button-toolbar'> <div class='button-toolbar container-fluid' style='float: right;'> - {% if perms.part.add_partparameter %} + {% if roles.part.add %} <button title='{% trans "Add new parameter" %}' class='btn btn-success' id='param-create'>{% trans "New Parameter" %}</button> {% endif %} </div> @@ -32,10 +32,10 @@ <td> {{ param.template.units }} <div class='btn-group' style='float: right;'> - {% if perms.part.change_partparameter %} + {% if roles.part.change %} <button title='{% trans "Edit" %}' class='btn btn-default btn-glyph param-edit' url="{% url 'part-param-edit' param.id %}" type='button'><span class='fas fa-edit'/></button> {% endif %} - {% if perms.part.delete_partparameter %} + {% if roles.part.delete %} <button title='{% trans "Delete" %}' class='btn btn-default btn-glyph param-delete' url="{% url 'part-param-delete' param.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button> {% endif %} </div> @@ -54,7 +54,7 @@ $('#param-table').inventreeTable({ }); - {% if perms.part.add_partparameter %} + {% if roles.part.add %} $('#param-create').click(function() { launchModalForm("{% url 'part-param-create' %}?part={{ part.id }}", { reload: true, diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 6103cb5369..750f3f3806 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -56,13 +56,13 @@ <button type='button' class='btn btn-default' id='price-button' title='{% trans "Show pricing information" %}'> <span id='part-price-icon' class='fas fa-dollar-sign'/> </button> - {% if perms.stock.change_stockitem %} + {% if roles.stock.change %} <button type='button' class='btn btn-default' id='part-count' title='{% trans "Count part stock" %}'> <span class='fas fa-clipboard-list'/> </button> {% endif %} {% if part.purchaseable %} - {% if perms.order.add_purchaseorder %} + {% if roles.purchase_order.add %} <button type='button' class='btn btn-default' id='part-order' title='{% trans "Order part" %}'> <span id='part-order-icon' class='fas fa-shopping-cart'/> </button> @@ -70,17 +70,17 @@ {% endif %} {% endif %} <!-- Part actions --> - {% if perms.part.add_part or perms.part.change_part or perms.part.delete_part %} + {% if roles.part.add or roles.part.change or roles.part.delete %} <div class='btn-group'> <button id='part-actions' title='{% trans "Part actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'> <span class='fas fa-shapes'></span> <span class='caret'></span></button> <ul class='dropdown-menu'> - {% if perms.part.add_part %} + {% if roles.part.add %} <li><a href='#' id='part-duplicate'><span class='fas fa-copy'></span> {% trans "Duplicate part" %}</a></li> {% endif %} - {% if perms.part.change_part %} + {% if roles.part.change %} <li><a href='#' id='part-edit'><span class='fas fa-edit icon-blue'></span> {% trans "Edit part" %}</a></li> {% endif %} - {% if not part.active and perms.part.delete_part %} + {% if not part.active and roles.part.delete %} <li><a href='#' id='part-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete part" %}</a></li> {% endif %} </ul> @@ -284,7 +284,7 @@ }); }); - {% if perms.part.change_part %} + {% if roles.part.change %} $("#part-edit").click(function() { launchModalForm( "{% url 'part-edit' part.id %}", @@ -304,7 +304,7 @@ }); }); - {% if perms.part.add_part %} + {% if roles.part.add %} $("#part-duplicate").click(function() { launchModalForm( "{% url 'part-duplicate' part.id %}", diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 02d8672979..e36675eeab 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -26,7 +26,7 @@ {% if part.assembly %} <li{% ifequal tab 'bom' %} class="active"{% endifequal %}> <a href="{% url 'part-bom' part.id %}">{% trans "BOM" %}<span class="badge{% if part.is_bom_valid == False %} badge-alert{% endif %}">{{ part.bom_count }}</span></a></li> - {% if perms.build.view_build %} + {% if roles.build.view %} <li{% ifequal tab 'build' %} class="active"{% endifequal %}> <a href="{% url 'part-build' part.id %}">{% trans "Build Orders" %}<span class='badge'>{{ part.builds.count }}</span></a> </li> diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 2224d85bc4..148a96c583 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -15,16 +15,16 @@ </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> - {% if perms.part.view_part or perms.part.view_partcategory %} + {% if roles.part.view %} <li><a href="{% url 'part-index' %}"><span class='fas fa-shapes icon-header'></span>{% trans "Parts" %}</a></li> {% endif %} - {% if perms.stock.view_stockitem or perms.part.view_stocklocation %} + {% if roles.stock.view %} <li><a href="{% url 'stock-index' %}"><span class='fas fa-boxes icon-header'></span>{% trans "Stock" %}</a></li> {% endif %} - {% if perms.build.view_build %} + {% if roles.build.view %} <li><a href="{% url 'build-index' %}"><span class='fas fa-tools icon-header'></span>{% trans "Build" %}</a></li> {% endif %} - {% if perms.order.view_purchaseorder %} + {% if roles.purchase_order.view %} <li class='nav navbar-nav'> <a class='dropdown-toggle' data-toggle='dropdown' href='#'><span class='fas fa-shopping-cart icon-header'></span>{% trans "Buy" %}</a> <ul class='dropdown-menu'> @@ -34,7 +34,7 @@ </ul> </li> {% endif %} - {% if perms.order.view_salesorder %} + {% if roles.sales_order.view %} <li class='nav navbar-nav'> <a class='dropdown-toggle' data-toggle='dropdown' href='#'><span class='fas fa-truck icon-header'></span>{% trans "Sell" %}</a> <ul class='dropdown-menu'> diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html index be26b1a976..b4b1b9d50f 100644 --- a/InvenTree/templates/stock_table.html +++ b/InvenTree/templates/stock_table.html @@ -6,21 +6,21 @@ <button class='btn btn-default' id='stock-export' title='{% trans "Export Stock Information" %}'>{% trans "Export" %}</button> {% if read_only %} {% else %} - {% if perms.stock.add_stockitem %} + {% if roles.stock.add %} <button class="btn btn-success" id='item-create'>{% trans "New Stock Item" %}</button> {% endif %} - {% if perms.stock.change_stockitem or perms.stock.delete_stockitem %} + {% if roles.stock.change or roles.stock.delete %} <div class="btn-group"> <button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button> <ul class="dropdown-menu"> - {% if perms.stock.change_stockitem %} + {% if roles.stock.change %} <li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'>{% trans "Add stock" %}</a></li> <li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'>{% trans "Remove stock" %}</a></li> <li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'>{% trans "Count stock" %}</a></li> <li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'>{% trans "Move stock" %}</a></li> <li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'>{% trans "Order stock" %}</a></li> {% endif %} - {% if perms.stock.delete_stockitem %} + {% if roles.stock.delete %} <li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'>{% trans "Delete Stock" %}</a></li> {% endif %} </ul> From dc2c9aa662e9a4af344ed60da0db84045da1e3a0 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 11:29:38 +1100 Subject: [PATCH 66/77] Add InvenTreeRoleMixin - Simplifies permission requirements for views - e.g. 'part.view' rather than 'part.view_partcategory' --- InvenTree/InvenTree/views.py | 54 ++++++++++++++++++++++++++++++++++++ InvenTree/part/api.py | 4 +++ InvenTree/part/views.py | 25 +++++++++-------- InvenTree/stock/api.py | 4 +++ InvenTree/users/models.py | 32 +++++++++++++++++++++ 5 files changed, 108 insertions(+), 11 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index d940229ebe..9cd4aeb514 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -22,6 +22,7 @@ from django.views.generic.base import TemplateView from part.models import Part, PartCategory from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme +from users.models import check_user_role, RuleSet from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm from .helpers import str2bool @@ -682,3 +683,56 @@ class DatabaseStatsView(AjaxView): """ return ctx + + +class InvenTreeRoleMixin(PermissionRequiredMixin): + """ + Permission class based on user roles, not user 'permissions'. + + To specify which role is required for the mixin, + set the class attribute 'role_required' to something like the following: + + role_required = 'part.add' + role_required = [ + 'part.change', + 'build.add', + ] + """ + + # By default, no roles are required + # Roles must be specified + role_required = None + + def has_permission(self): + """ + Determine if the current user + """ + + roles_required = [] + + if type(self.role_required) is str: + roles_required.append(self.role_required) + elif type(self.role_required) in [list, tuple]: + roles_required = self.role_required + + # List of permissions that will be required + permissions = [] + + user = self.request.user + + # Superuser can have any permissions they desire + if user.is_superuser: + return True + + print(type(self), "Required roles:", roles_required) + + for required in roles_required: + + (role, permission) = required.split('.') + + # Return False if the user does not have *any* of the required roles + if not check_user_role(user, role, permission): + return False + + # We did not fail any required checks + return True diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index d4aeec5bd9..d643b8671a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -44,6 +44,10 @@ class PartCategoryTree(TreeSerializer): def get_items(self): return PartCategory.objects.all().prefetch_related('parts', 'children') + permission_classes = [ + permissions.IsAuthenticated, + ] + class CategoryList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartCategory objects. diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 8e0980c35e..e352cddb08 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -12,7 +12,6 @@ from django.shortcuts import HttpResponseRedirect from django.utils.translation import gettext_lazy as _ from django.urls import reverse, reverse_lazy from django.views.generic import DetailView, ListView, FormView, UpdateView -from django.contrib.auth.mixins import PermissionRequiredMixin from django.forms.models import model_to_dict from django.forms import HiddenInput, CheckboxInput from django.conf import settings @@ -39,17 +38,21 @@ from .admin import PartResource from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView +from InvenTree.views import InvenTreeRoleMixin from InvenTree.helpers import DownloadFile, str2bool -class PartIndex(PermissionRequiredMixin, ListView): +class PartIndex(InvenTreeRoleMixin, ListView): """ View for displaying list of Part objects """ + model = Part template_name = 'part/category.html' context_object_name = 'parts' - permission_required = ('part.view_part', 'part.view_partcategory') + + role_required = 'part.view' + def get_queryset(self): return Part.objects.all().select_related('category') @@ -658,7 +661,7 @@ class PartNotes(UpdateView): return ctx -class PartDetail(PermissionRequiredMixin, DetailView): +class PartDetail(InvenTreeRoleMixin, DetailView): """ Detail view for Part object """ @@ -666,7 +669,7 @@ class PartDetail(PermissionRequiredMixin, DetailView): queryset = Part.objects.all().select_related('category') template_name = 'part/detail.html' - permission_required = 'part.view_part' + role_required = 'part.view' # Add in some extra context information based on query params def get_context_data(self, **kwargs): @@ -869,7 +872,7 @@ class BomValidate(AjaxUpdateView): return self.renderJsonResponse(request, form, data, context=self.get_context()) -class BomUpload(PermissionRequiredMixin, FormView): +class BomUpload(InvenTreeRoleMixin, FormView): """ View for uploading a BOM file, and handling BOM data importing. The BOM upload process is as follows: @@ -905,7 +908,7 @@ class BomUpload(PermissionRequiredMixin, FormView): missing_columns = [] allowed_parts = [] - permission_required = ('part.change_part', 'part.add_bomitem') + role_required = ('part.change', 'part.add') def get_success_url(self): part = self.get_object() @@ -1931,7 +1934,7 @@ class PartParameterDelete(AjaxDeleteView): ajax_form_title = _('Delete Part Parameter') -class CategoryDetail(PermissionRequiredMixin, DetailView): +class CategoryDetail(InvenTreeRoleMixin, DetailView): """ Detail view for PartCategory """ model = PartCategory @@ -1939,7 +1942,7 @@ class CategoryDetail(PermissionRequiredMixin, DetailView): queryset = PartCategory.objects.all().prefetch_related('children') template_name = 'part/category_partlist.html' - permission_required = 'part.view_partcategory' + role_required = 'part.view' def get_context_data(self, **kwargs): @@ -2081,13 +2084,13 @@ class CategoryCreate(AjaxCreateView): return initials -class BomItemDetail(PermissionRequiredMixin, DetailView): +class BomItemDetail(InvenTreeRoleMixin, DetailView): """ Detail view for BomItem """ context_object_name = 'item' queryset = BomItem.objects.all() template_name = 'part/bom-detail.html' - permission_required = 'part.view_bomitem' + role_required = 'part.view' class BomItemCreate(AjaxCreateView): diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 790de7d879..ba802b75d9 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -52,6 +52,10 @@ class StockCategoryTree(TreeSerializer): def get_items(self): return StockLocation.objects.all().prefetch_related('stock_items', 'children') + permission_classes = [ + permissions.IsAuthenticated, + ] + class StockDetail(generics.RetrieveUpdateDestroyAPIView): """ API detail endpoint for Stock object diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 3bd976f0a4..24b0318695 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -329,3 +329,35 @@ def create_missing_rule_sets(sender, instance, **kwargs): """ update_group_roles(instance) + + +def check_user_role(user, role, permission): + """ + Check if a user has a particular role:permission combination. + + If the user is a superuser, this will return True + """ + + if user.is_superuser: + return True + + for group in user.groups.all(): + + for rule in group.rule_sets.all(): + + if rule.name == role: + + if permission == 'add' and rule.can_add: + return True + + if permission == 'change' and rule.can_change: + return True + + if permission == 'view' and rule.can_view: + return True + + if permission == 'delete' and rule.can_delete: + return True + + # No matching permissions found + return False From c740cce5e435523e800e15dcb73433c1673b2f46 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 11:31:04 +1100 Subject: [PATCH 67/77] PEP fixes --- InvenTree/InvenTree/views.py | 9 +++------ InvenTree/part/views.py | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 9cd4aeb514..ca8f6c646c 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -22,7 +22,7 @@ from django.views.generic.base import TemplateView from part.models import Part, PartCategory from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme -from users.models import check_user_role, RuleSet +from users.models import check_user_role from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm from .helpers import str2bool @@ -700,12 +700,12 @@ class InvenTreeRoleMixin(PermissionRequiredMixin): """ # By default, no roles are required - # Roles must be specified + # Roles must be specified role_required = None def has_permission(self): """ - Determine if the current user + Determine if the current user """ roles_required = [] @@ -715,9 +715,6 @@ class InvenTreeRoleMixin(PermissionRequiredMixin): elif type(self.role_required) in [list, tuple]: roles_required = self.role_required - # List of permissions that will be required - permissions = [] - user = self.request.user # Superuser can have any permissions they desire diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index e352cddb08..09390bf8c0 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -53,7 +53,6 @@ class PartIndex(InvenTreeRoleMixin, ListView): role_required = 'part.view' - def get_queryset(self): return Part.objects.all().select_related('category') From 11d31960c7c64b78b6ced94fe662d611119596a9 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 11:42:16 +1100 Subject: [PATCH 68/77] Change modal form permissions to use new "role" strategy --- InvenTree/InvenTree/views.py | 119 +++++++++++++++-------------------- InvenTree/part/views.py | 74 +++++++++++----------- 2 files changed, 89 insertions(+), 104 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index ca8f6c646c..198903db9a 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -108,31 +108,66 @@ class TreeSerializer(views.APIView): return JsonResponse(response, safe=False) -class AjaxMixin(PermissionRequiredMixin): +class InvenTreeRoleMixin(PermissionRequiredMixin): + """ + Permission class based on user roles, not user 'permissions'. + + To specify which role is required for the mixin, + set the class attribute 'role_required' to something like the following: + + role_required = 'part.add' + role_required = [ + 'part.change', + 'build.add', + ] + """ + + # By default, no roles are required + # Roles must be specified + role_required = None + + def has_permission(self): + """ + Determine if the current user + """ + + roles_required = [] + + if type(self.role_required) is str: + roles_required.append(self.role_required) + elif type(self.role_required) in [list, tuple]: + roles_required = self.role_required + + user = self.request.user + + # Superuser can have any permissions they desire + if user.is_superuser: + return True + + for required in roles_required: + + (role, permission) = required.split('.') + + # Return False if the user does not have *any* of the required roles + if not check_user_role(user, role, permission): + return False + + # We did not fail any required checks + return True + + +class AjaxMixin(InvenTreeRoleMixin): """ AjaxMixin provides basic functionality for rendering a Django form to JSON. Handles jsonResponse rendering, and adds extra data for the modal forms to process on the client side. Any view which inherits the AjaxMixin will need - correct permissions set using the 'permission_required' attribute + correct permissions set using the 'role_required' attribute """ - # By default, allow *any* permissions - permission_required = '*' - - def has_permission(self): - """ - Override the default behaviour of has_permission from PermissionRequiredMixin. - - Basically, if permission_required attribute = '*', - no permissions are actually required! - """ - - if self.permission_required == '*': - return True - else: - return super().has_permission() + # By default, allow *any* role + role_required = None # By default, point to the modal_form template # (this can be overridden by a child class) @@ -683,53 +718,3 @@ class DatabaseStatsView(AjaxView): """ return ctx - - -class InvenTreeRoleMixin(PermissionRequiredMixin): - """ - Permission class based on user roles, not user 'permissions'. - - To specify which role is required for the mixin, - set the class attribute 'role_required' to something like the following: - - role_required = 'part.add' - role_required = [ - 'part.change', - 'build.add', - ] - """ - - # By default, no roles are required - # Roles must be specified - role_required = None - - def has_permission(self): - """ - Determine if the current user - """ - - roles_required = [] - - if type(self.role_required) is str: - roles_required.append(self.role_required) - elif type(self.role_required) in [list, tuple]: - roles_required = self.role_required - - user = self.request.user - - # Superuser can have any permissions they desire - if user.is_superuser: - return True - - print(type(self), "Required roles:", roles_required) - - for required in roles_required: - - (role, permission) = required.split('.') - - # Return False if the user does not have *any* of the required roles - if not check_user_role(user, role, permission): - return False - - # We did not fail any required checks - return True diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 09390bf8c0..6498774285 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -80,7 +80,7 @@ class PartAttachmentCreate(AjaxCreateView): ajax_form_title = _("Add part attachment") ajax_template_name = "modal_form.html" - permission_required = 'part.add_partattachment' + role_required = 'part.add' def post_save(self): """ Record the user that uploaded the attachment """ @@ -130,7 +130,7 @@ class PartAttachmentEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit attachment') - permission_required = 'part.change_partattachment' + role_required = 'part.change' def get_data(self): return { @@ -153,7 +153,7 @@ class PartAttachmentDelete(AjaxDeleteView): ajax_template_name = "attachment_delete.html" context_object_name = "attachment" - permission_required = 'part.delete_partattachment' + role_required = 'part.delete' def get_data(self): return { @@ -168,7 +168,7 @@ class PartTestTemplateCreate(AjaxCreateView): form_class = part_forms.EditPartTestTemplateForm ajax_form_title = _("Create Test Template") - permission_required = 'part.add_parttesttemplate' + role_required = 'part.add' def get_initial(self): @@ -197,7 +197,7 @@ class PartTestTemplateEdit(AjaxUpdateView): form_class = part_forms.EditPartTestTemplateForm ajax_form_title = _("Edit Test Template") - permission_required = 'part.change_parttesttemplate' + role_required = 'part.change' def get_form(self): @@ -213,7 +213,7 @@ class PartTestTemplateDelete(AjaxDeleteView): model = PartTestTemplate ajax_form_title = _("Delete Test Template") - permission_required = 'part.delete_parttesttemplate' + role_required = 'part.delete' class PartSetCategory(AjaxUpdateView): @@ -223,7 +223,7 @@ class PartSetCategory(AjaxUpdateView): ajax_form_title = _('Set Part Category') form_class = part_forms.SetPartCategoryForm - permission_required = 'part.change_part' + role_required = 'part.change' category = None parts = [] @@ -308,7 +308,7 @@ class MakePartVariant(AjaxCreateView): ajax_form_title = _('Create Variant') ajax_template_name = 'part/variant_part.html' - permission_required = 'part.add_part' + role_required = 'part.add' def get_part_template(self): return get_object_or_404(Part, id=self.kwargs['pk']) @@ -388,7 +388,7 @@ class PartDuplicate(AjaxCreateView): ajax_form_title = _("Duplicate Part") ajax_template_name = "part/copy_part.html" - permission_required = 'part.add_part' + role_required = 'part.add' def get_data(self): return { @@ -513,7 +513,7 @@ class PartCreate(AjaxCreateView): ajax_form_title = _('Create new part') ajax_template_name = 'part/create_part.html' - permission_required = 'part.add_part' + role_required = 'part.add' def get_data(self): return { @@ -637,7 +637,7 @@ class PartNotes(UpdateView): template_name = 'part/notes.html' model = Part - permission_required = 'part.change_part' + role_required = 'part.change' fields = ['notes'] @@ -734,7 +734,7 @@ class PartQRCode(QRCodeView): ajax_form_title = _("Part QR Code") - permission_required = 'part.view_part' + role_required = 'part.view' def get_qr_data(self): """ Generate QR code data for the Part """ @@ -755,7 +755,7 @@ class PartImageUpload(AjaxUpdateView): form_class = part_forms.PartImageForm - permission_required = 'part.change_part' + role_required = 'part.change' def get_data(self): return { @@ -770,7 +770,7 @@ class PartImageSelect(AjaxUpdateView): ajax_template_name = 'part/select_image.html' ajax_form_title = _('Select Part Image') - permission_required = 'part.change_part' + role_required = 'part.change' fields = [ 'image', @@ -813,7 +813,7 @@ class PartEdit(AjaxUpdateView): ajax_form_title = _('Edit Part Properties') context_object_name = 'part' - permission_required = 'part.change_part' + role_required = 'part.change' def get_form(self): """ Create form for Part editing. @@ -839,7 +839,7 @@ class BomValidate(AjaxUpdateView): context_object_name = 'part' form_class = part_forms.BomValidateForm - permission_required = ('part.change_part') + role_required = 'part.change' def get_context(self): return { @@ -1507,7 +1507,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): class PartExport(AjaxView): """ Export a CSV file containing information on multiple parts """ - permission_required = 'part.view_part' + role_required = 'part.view' def get_parts(self, request): """ Extract part list from the POST parameters. @@ -1586,7 +1586,7 @@ class BomDownload(AjaxView): - File format should be passed as a query param e.g. ?format=csv """ - permission_required = ('part.view_part', 'part.view_bomitem') + role_required = 'part.view' model = Part @@ -1641,7 +1641,7 @@ class BomExport(AjaxView): form_class = part_forms.BomExportForm ajax_form_title = _("Export Bill of Materials") - permission_required = ('part.view_part', 'part.view_bomitem') + role_required = 'part.view' def get(self, request, *args, **kwargs): return self.renderJsonResponse(request, self.form_class()) @@ -1692,7 +1692,7 @@ class PartDelete(AjaxDeleteView): ajax_form_title = _('Confirm Part Deletion') context_object_name = 'part' - permission_required = 'part.delete_part' + role_required = 'part.delete' success_url = '/part/' @@ -1710,8 +1710,8 @@ class PartPricing(AjaxView): ajax_form_title = _("Part Pricing") form_class = part_forms.PartPriceForm - permission_required = ('company.view_supplierpricebreak', 'part.view_part') - + role_required = ['sales_order.view', 'part.view'] + def get_part(self): try: return Part.objects.get(id=self.kwargs['pk']) @@ -1829,7 +1829,7 @@ class PartPricing(AjaxView): class PartParameterTemplateCreate(AjaxCreateView): """ View for creating a new PartParameterTemplate """ - permission_required = 'part.add_partparametertemplate' + role_required = 'part.add' model = PartParameterTemplate form_class = part_forms.EditPartParameterTemplateForm @@ -1839,7 +1839,7 @@ class PartParameterTemplateCreate(AjaxCreateView): class PartParameterTemplateEdit(AjaxUpdateView): """ View for editing a PartParameterTemplate """ - permission_required = 'part.change_partparametertemplate' + role_required = 'part.change' model = PartParameterTemplate form_class = part_forms.EditPartParameterTemplateForm @@ -1849,7 +1849,7 @@ class PartParameterTemplateEdit(AjaxUpdateView): class PartParameterTemplateDelete(AjaxDeleteView): """ View for deleting an existing PartParameterTemplate """ - permission_required = 'part.delete_partparametertemplate' + role_required = 'part.delete' model = PartParameterTemplate ajax_form_title = _("Delete Part Parameter Template") @@ -1858,7 +1858,7 @@ class PartParameterTemplateDelete(AjaxDeleteView): class PartParameterCreate(AjaxCreateView): """ View for creating a new PartParameter """ - permission_required = 'part.add_partparameter' + role_required = 'part.add' model = PartParameter form_class = part_forms.EditPartParameterForm @@ -1910,7 +1910,7 @@ class PartParameterCreate(AjaxCreateView): class PartParameterEdit(AjaxUpdateView): """ View for editing a PartParameter """ - permission_required = 'part.change_partparameter' + role_required = 'part.change' model = PartParameter form_class = part_forms.EditPartParameterForm @@ -1926,7 +1926,7 @@ class PartParameterEdit(AjaxUpdateView): class PartParameterDelete(AjaxDeleteView): """ View for deleting a PartParameter """ - permission_required = 'part.delete_partparameter' + role_required = 'part.delete' model = PartParameter ajax_template_name = 'part/param_delete.html' @@ -1991,7 +1991,7 @@ class CategoryEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit Part Category') - permission_required = 'part.change_partcategory' + role_required = 'part.change' def get_context_data(self, **kwargs): context = super(CategoryEdit, self).get_context_data(**kwargs).copy() @@ -2030,7 +2030,7 @@ class CategoryDelete(AjaxDeleteView): context_object_name = 'category' success_url = '/part/' - permission_required = 'part.delete_partcategory' + role_required = 'part.delete' def get_data(self): return { @@ -2046,7 +2046,7 @@ class CategoryCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' form_class = part_forms.EditCategoryForm - permission_required = 'part.add_partcategory' + role_required = 'part.add' def get_context_data(self, **kwargs): """ Add extra context data to template. @@ -2099,7 +2099,7 @@ class BomItemCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Create BOM item') - permission_required = 'part.add_bomitem' + role_required = 'part.add' def get_form(self): """ Override get_form() method to reduce Part selection options. @@ -2167,7 +2167,7 @@ class BomItemEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit BOM item') - permission_required = 'part.change_bomitem' + role_required = 'part.change' def get_form(self): """ Override get_form() method to filter part selection options @@ -2217,7 +2217,7 @@ class BomItemDelete(AjaxDeleteView): context_object_name = 'item' ajax_form_title = _('Confim BOM item deletion') - permission_required = 'part.delete_bomitem' + role_required = 'part.delete' class PartSalePriceBreakCreate(AjaxCreateView): @@ -2227,7 +2227,7 @@ class PartSalePriceBreakCreate(AjaxCreateView): form_class = part_forms.EditPartSalePriceBreakForm ajax_form_title = _('Add Price Break') - permission_required = 'part.add_partsellpricebreak' + role_required = 'part.add' def get_data(self): return { @@ -2278,7 +2278,7 @@ class PartSalePriceBreakEdit(AjaxUpdateView): form_class = part_forms.EditPartSalePriceBreakForm ajax_form_title = _('Edit Price Break') - permission_required = 'part.change_partsellpricebreak' + role_required = 'part.change' def get_form(self): @@ -2295,4 +2295,4 @@ class PartSalePriceBreakDelete(AjaxDeleteView): ajax_form_title = _("Delete Price Break") ajax_template_name = "modal_delete_form.html" - permission_required = 'part.delete_partsalepricebreak' + role_required = 'part.delete' From d691b15f4b0a42f2356c6c1563c96a862e7c8a75 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 12:34:30 +1100 Subject: [PATCH 69/77] Fix conflicts --- InvenTree/users/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 24b0318695..773dec36f7 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -328,7 +328,12 @@ def create_missing_rule_sets(sender, instance, **kwargs): group permissions. """ - update_group_roles(instance) + created = kwargs.get('created', False) + # To trigger the group permissions update: update_fields should not be None + update_fields = kwargs.get('update_fields', None) + + if created or update_fields: + update_group_roles(instance) def check_user_role(user, role, permission): From b3e4efd96e79cfe1562d249bbd17502c08e12deb Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 16:03:19 +1100 Subject: [PATCH 70/77] Unit testing fixes --- InvenTree/part/test_api.py | 35 ++++++++++++++--------------------- InvenTree/part/test_views.py | 31 +++++++++++-------------------- InvenTree/users/models.py | 11 +++++------ 3 files changed, 30 insertions(+), 47 deletions(-) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 9fdc2688cb..328ad3ef9e 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -3,13 +3,13 @@ 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 stock.models import StockItem from company.models import Company from InvenTree.status_codes import StockStatus -from InvenTree.helpers import addUserPermissions class PartAPITest(APITestCase): @@ -36,27 +36,20 @@ class PartAPITest(APITestCase): password='password' ) - # Add the permissions required to access the API endpoints - perms = [ - 'view_part', - 'add_part', - 'change_part', - 'delete_part', - 'view_partcategory', - 'add_partcategory', - 'change_partcategory', - 'view_bomitem', - 'add_bomitem', - 'change_bomitem', - 'view_partattachment', - 'change_partattachment', - 'add_partattachment', - 'view_parttesttemplate', - 'add_parttesttemplate', - 'change_parttesttemplate', - ] + # Put the user into a group with the correct permissions + group = Group.objects.create(name='mygroup') + self.user.groups.add(group) - addUserPermissions(self.user, perms) + # 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') diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index b1ae991a0c..d8c345d243 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -3,8 +3,7 @@ from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model - -from InvenTree.helpers import addUserPermissions +from django.contrib.auth.models import Group from .models import Part @@ -31,26 +30,18 @@ class PartViewTestCase(TestCase): password='password' ) - # Add the permissions required to access the pages - perms = [ - 'view_part', - 'add_part', - 'change_part', - 'delete_part', - 'view_partcategory', - 'add_partcategory', - 'change_partcategory', - 'view_bomitem', - 'add_bomitem', - 'change_bomitem', - 'view_partattachment', - 'change_partattachment', - 'add_partattachment', - ] + # Put the user into a group with the correct permissions + group = Group.objects.create(name='mygroup') + self.user.groups.add(group) - addUserPermissions(self.user, perms) + # 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 - self.user.save() + rule.save() self.client.login(username='username', password='password') diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 773dec36f7..050bbe2ef9 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -171,6 +171,10 @@ class RuleSet(models.Model): super().save(*args, **kwargs) + if self.group: + # Update the group too! + self.group.save() + def get_models(self): """ Return the database tables / models that this ruleset covers. @@ -328,12 +332,7 @@ def create_missing_rule_sets(sender, instance, **kwargs): group permissions. """ - created = kwargs.get('created', False) - # To trigger the group permissions update: update_fields should not be None - update_fields = kwargs.get('update_fields', None) - - if created or update_fields: - update_group_roles(instance) + update_group_roles(instance) def check_user_role(user, role, permission): From 88f73443ee6922980356adc15e28aa6df55875b4 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 16:43:39 +1100 Subject: [PATCH 71/77] Yet more style fixes --- InvenTree/users/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 6513e979aa..fcf137bb01 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -338,9 +338,6 @@ def create_missing_rule_sets(sender, instance, **kwargs): then we can now use these RuleSet values to update the group permissions. """ - created = kwargs.get('created', False) - # To trigger the group permissions update: update_fields should not be None - update_fields = kwargs.get('update_fields', None) update_group_roles(instance) From ab454e5ba4fdac8567ef2f7656fb60120f20a205 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 16:46:13 +1100 Subject: [PATCH 72/77] More template changes: perms -> roles --- InvenTree/build/templates/build/build_base.html | 2 +- InvenTree/company/templates/company/company_base.html | 2 +- InvenTree/order/templates/order/order_base.html | 2 +- InvenTree/order/templates/order/sales_order_base.html | 2 +- InvenTree/part/templates/part/part_base.html | 4 ++-- InvenTree/part/templates/part/tabs.html | 4 ++-- InvenTree/stock/templates/stock/item_base.html | 2 +- InvenTree/stock/templates/stock/location.html | 2 +- InvenTree/templates/slide.html | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 915433b055..ed3da576d5 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -35,7 +35,7 @@ src="{% static 'img/blank_image.png' %}" <hr> <h4> {{ build.quantity }} x {{ build.part.full_name }} - {% if user.is_staff and perms.build.change_build %} + {% if user.is_staff and roles.build.change %} <a href="{% url 'admin:build_build_change' build.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a> {% endif %} </h4> diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 73ebecf979..f20107277d 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -23,7 +23,7 @@ InvenTree | {% trans "Company" %} - {{ company.name }} <hr> <h4> {{ company.name }} - {% if user.is_staff and perms.company.change_company %} + {% if user.is_staff and roles.company.change %} <a href="{% url 'admin:company_company_change' company.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a> {% endif %} </h4> diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 036021b12d..71e3455722 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -24,7 +24,7 @@ src="{% static 'img/blank_image.png' %}" <hr> <h4> {{ order }} - {% if user.is_staff and perms.order.change_purchaseorder %} + {% if user.is_staff and roles.purchase_order.change %} <a href="{% url 'admin:order_purchaseorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a> {% endif %} </h4> diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 70505ddccc..0572104e09 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -34,7 +34,7 @@ src="{% static 'img/blank_image.png' %}" <hr> <h4> {{ order }} - {% if user.is_staff and perms.order.change_salesorder %} + {% if user.is_staff and roles.sales_order.change %} <a href="{% url 'admin:order_salesorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a> {% endif %} </h4> diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 750f3f3806..d7604deae4 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -28,7 +28,7 @@ <div class="media-body"> <h3> {{ part.full_name }} - {% if user.is_staff and perms.part.change_part %} + {% if user.is_staff and roles.part.change %} <a href="{% url 'admin:part_part_change' part.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a> {% endif %} {% if not part.active %} @@ -315,7 +315,7 @@ }); {% endif %} - {% if not part.active and perms.part.delete_part %} + {% if not part.active and roles.part.delete %} $("#part-delete").click(function() { launchModalForm( "{% url 'part-delete' part.id %}", diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index e36675eeab..8322a225bc 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -36,7 +36,7 @@ <li{% ifequal tab 'used' %} class="active"{% endifequal %}> <a href="{% url 'part-used-in' part.id %}">{% trans "Used In" %} {% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li> {% endif %} - {% if part.purchaseable and perms.order.view_purchaseorder %} + {% if part.purchaseable and roles.purchase_order.view %} {% if part.is_template == False %} <li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}> <a href="{% url 'part-suppliers' part.id %}">{% trans "Suppliers" %} @@ -48,7 +48,7 @@ <a href="{% url 'part-orders' part.id %}">{% trans "Purchase Orders" %} <span class='badge'>{{ part.purchase_orders|length }}</span></a> </li> {% endif %} - {% if part.salable and perms.order.view_salesorder %} + {% if part.salable and roles.sales_order.view %} <li {% if tab == 'sales-prices' %}class='active'{% endif %}> <a href="{% url 'part-sale-prices' part.id %}">{% trans "Sale Price" %}</a> </li> diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index b3fb9af743..928aa6b7a1 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -65,7 +65,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% else %} <a href='{% url "part-detail" item.part.pk %}'>{{ item.part.full_name }}</a> × {% decimal item.quantity %} {% endif %} -{% if user.is_staff and perms.stock.change_stockitem %} +{% if user.is_staff and roles.stock.change %} <a href="{% url 'admin:stock_stockitem_change' item.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a> {% endif %} </h4> diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 2f319f4925..d411891078 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -8,7 +8,7 @@ {% if location %} <h3> {{ location.name }} - {% if user.is_staff and perms.stock.change_stocklocation %} + {% if user.is_staff and roles.stock.change %} <a href="{% url 'admin:stock_stocklocation_change' location.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a> {% endif %} </h3> diff --git a/InvenTree/templates/slide.html b/InvenTree/templates/slide.html index 45535786c9..edd39e75a2 100644 --- a/InvenTree/templates/slide.html +++ b/InvenTree/templates/slide.html @@ -1,3 +1,3 @@ <div> - <input fieldname='{{ field }}' class='slidey' type="checkbox" data-offstyle='warning' data-onstyle="success" data-size='small' data-toggle="toggle" {% if disabled or not perms.part.change_part %}disabled {% endif %}{% if state %}checked=""{% endif %} autocomplete="off"> + <input fieldname='{{ field }}' class='slidey' type="checkbox" data-offstyle='warning' data-onstyle="success" data-size='small' data-toggle="toggle" {% if disabled or not roles.part.change %}disabled {% endif %}{% if state %}checked=""{% endif %} autocomplete="off"> </div> \ No newline at end of file From 1c97aaf87a4474df73198b2b6d46f702ff802b66 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 19:46:53 +1100 Subject: [PATCH 73/77] Set permissions for order views --- InvenTree/InvenTree/views.py | 8 ++- .../templates/order/sales_order_base.html | 2 +- InvenTree/order/views.py | 53 ++++++++++++++++--- InvenTree/users/models.py | 4 ++ 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 198903db9a..bb7c1e6f5d 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -22,7 +22,7 @@ from django.views.generic.base import TemplateView from part.models import Part, PartCategory from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme -from users.models import check_user_role +from users.models import check_user_role, RuleSet from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm from .helpers import str2bool @@ -147,7 +147,13 @@ class InvenTreeRoleMixin(PermissionRequiredMixin): for required in roles_required: (role, permission) = required.split('.') + + if role not in RuleSet.RULESET_NAMES: + raise ValueError(f"Role '{role}' is not a valid role") + if permission not in RuleSet.RULESET_PERMISSIONS: + raise ValueError(f"Permission '{permission}' is not a valid permission") + # Return False if the user does not have *any* of the required roles if not check_user_role(user, role, permission): return False diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 0572104e09..52856cadbd 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -44,7 +44,7 @@ src="{% static 'img/blank_image.png' %}" <button type='button' class='btn btn-default' id='edit-order' title='Edit order information'> <span class='fas fa-edit icon-green'></span> </button> - <button type='button' class='btn btn-default' id='packing-list' title='{% trans "Packing List" %}'> + <button type='button' disabled='' class='btn btn-default' id='packing-list' title='{% trans "Packing List" %}'> <span class='fas fa-clipboard-list'></span> </button> {% if order.status == SalesOrderStatus.PENDING %} diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 5d140fb77c..28b8416b65 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -28,19 +28,22 @@ from . import forms as order_forms from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.helpers import DownloadFile, str2bool +from InvenTree.views import InvenTreeRoleMixin from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus logger = logging.getLogger(__name__) -class PurchaseOrderIndex(ListView): +class PurchaseOrderIndex(InvenTreeRoleMixin, ListView): """ List view for all purchase orders """ model = PurchaseOrder template_name = 'order/purchase_orders.html' context_object_name = 'orders' + role_required = 'purchase_order.view' + def get_queryset(self): """ Retrieve the list of purchase orders, ensure that the most recent ones are returned first. """ @@ -55,19 +58,21 @@ class PurchaseOrderIndex(ListView): return ctx -class SalesOrderIndex(ListView): +class SalesOrderIndex(InvenTreeRoleMixin, ListView): model = SalesOrder template_name = 'order/sales_orders.html' context_object_name = 'orders' + role_required = 'sales_order.view' -class PurchaseOrderDetail(DetailView): +class PurchaseOrderDetail(InvenTreeRoleMixin, DetailView): """ Detail view for a PurchaseOrder object """ context_object_name = 'order' queryset = PurchaseOrder.objects.all().prefetch_related('lines') template_name = 'order/purchase_order_detail.html' + role_required = 'purchase_order.view' def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) @@ -75,12 +80,13 @@ class PurchaseOrderDetail(DetailView): return ctx -class SalesOrderDetail(DetailView): +class SalesOrderDetail(InvenTreeRoleMixin, DetailView): """ Detail view for a SalesOrder object """ context_object_name = 'order' queryset = SalesOrder.objects.all().prefetch_related('lines') template_name = 'order/sales_order_detail.html' + role_required = 'sales_order.view' class PurchaseOrderAttachmentCreate(AjaxCreateView): @@ -92,6 +98,7 @@ class PurchaseOrderAttachmentCreate(AjaxCreateView): form_class = order_forms.EditPurchaseOrderAttachmentForm ajax_form_title = _("Add Purchase Order Attachment") ajax_template_name = "modal_form.html" + role_required = 'purchase_order.add' def post_save(self, **kwargs): self.object.user = self.request.user @@ -139,6 +146,7 @@ class SalesOrderAttachmentCreate(AjaxCreateView): model = SalesOrderAttachment form_class = order_forms.EditSalesOrderAttachmentForm ajax_form_title = _('Add Sales Order Attachment') + role_required = 'sales_order.add' def post_save(self, **kwargs): self.object.user = self.request.user @@ -174,6 +182,7 @@ class PurchaseOrderAttachmentEdit(AjaxUpdateView): model = PurchaseOrderAttachment form_class = order_forms.EditPurchaseOrderAttachmentForm ajax_form_title = _("Edit Attachment") + role_required = 'purchase_order.change' def get_data(self): return { @@ -195,6 +204,7 @@ class SalesOrderAttachmentEdit(AjaxUpdateView): model = SalesOrderAttachment form_class = order_forms.EditSalesOrderAttachmentForm ajax_form_title = _("Edit Attachment") + role_required = 'sales_order.change' def get_data(self): return { @@ -216,6 +226,7 @@ class PurchaseOrderAttachmentDelete(AjaxDeleteView): ajax_form_title = _("Delete Attachment") ajax_template_name = "order/delete_attachment.html" context_object_name = "attachment" + role_required = 'purchase_order.delete' def get_data(self): return { @@ -230,6 +241,7 @@ class SalesOrderAttachmentDelete(AjaxDeleteView): ajax_form_title = _("Delete Attachment") ajax_template_name = "order/delete_attachment.html" context_object_name = "attachment" + role_required = 'sales_order.delete' def get_data(self): return { @@ -237,12 +249,13 @@ class SalesOrderAttachmentDelete(AjaxDeleteView): } -class PurchaseOrderNotes(UpdateView): +class PurchaseOrderNotes(InvenTreeRoleMixin, UpdateView): """ View for updating the 'notes' field of a PurchaseOrder """ context_object_name = 'order' template_name = 'order/order_notes.html' model = PurchaseOrder + role_required = 'purchase_order.change' fields = ['notes'] @@ -259,12 +272,13 @@ class PurchaseOrderNotes(UpdateView): return ctx -class SalesOrderNotes(UpdateView): +class SalesOrderNotes(InvenTreeRoleMixin, UpdateView): """ View for editing the 'notes' field of a SalesORder """ context_object_name = 'order' template_name = 'order/sales_order_notes.html' model = SalesOrder + role_required = 'sales_order.view' fields = ['notes'] @@ -286,6 +300,7 @@ class PurchaseOrderCreate(AjaxCreateView): model = PurchaseOrder ajax_form_title = _("Create Purchase Order") form_class = order_forms.EditPurchaseOrderForm + role_required = 'purchase_order.add' def get_initial(self): initials = super().get_initial().copy() @@ -317,6 +332,7 @@ class SalesOrderCreate(AjaxCreateView): model = SalesOrder ajax_form_title = _("Create Sales Order") form_class = order_forms.EditSalesOrderForm + role_required = 'sales_order.add' def get_initial(self): initials = super().get_initial().copy() @@ -347,6 +363,7 @@ class PurchaseOrderEdit(AjaxUpdateView): model = PurchaseOrder ajax_form_title = _('Edit Purchase Order') form_class = order_forms.EditPurchaseOrderForm + role_required = 'purchase_order.change' def get_form(self): @@ -367,6 +384,7 @@ class SalesOrderEdit(AjaxUpdateView): model = SalesOrder ajax_form_title = _('Edit Sales Order') form_class = order_forms.EditSalesOrderForm + role_required = 'sales_order.change' def get_form(self): form = super().get_form() @@ -384,6 +402,7 @@ class PurchaseOrderCancel(AjaxUpdateView): ajax_form_title = _('Cancel Order') ajax_template_name = 'order/order_cancel.html' form_class = order_forms.CancelPurchaseOrderForm + role_required = 'purchase_order.change' def post(self, request, *args, **kwargs): """ Mark the PO as 'CANCELLED' """ @@ -417,6 +436,7 @@ class SalesOrderCancel(AjaxUpdateView): ajax_form_title = _("Cancel sales order") ajax_template_name = "order/sales_order_cancel.html" form_class = order_forms.CancelSalesOrderForm + role_required = 'sales_order.change' def post(self, request, *args, **kwargs): @@ -451,6 +471,7 @@ class PurchaseOrderIssue(AjaxUpdateView): ajax_form_title = _('Issue Order') ajax_template_name = "order/order_issue.html" form_class = order_forms.IssuePurchaseOrderForm + role_required = 'purchase_order.change' def post(self, request, *args, **kwargs): """ Mark the purchase order as 'PLACED' """ @@ -486,6 +507,7 @@ class PurchaseOrderComplete(AjaxUpdateView): ajax_template_name = "order/order_complete.html" ajax_form_title = _("Complete Order") context_object_name = 'order' + role_required = 'purchase_order.change' def get_context_data(self): @@ -520,6 +542,7 @@ class SalesOrderShip(AjaxUpdateView): context_object_name = 'order' ajax_template_name = 'order/sales_order_ship.html' ajax_form_title = _('Ship Order') + role_required = 'sales_order.change' def post(self, request, *args, **kwargs): @@ -563,6 +586,7 @@ class PurchaseOrderExport(AjaxView): """ model = PurchaseOrder + role_required = 'purchase_order.view' def get(self, request, *args, **kwargs): @@ -594,6 +618,7 @@ class PurchaseOrderReceive(AjaxUpdateView): form_class = order_forms.ReceivePurchaseOrderForm ajax_form_title = _("Receive Parts") ajax_template_name = "order/receive_parts.html" + role_required = 'purchase_order.change' # Where the parts will be going (selected in POST request) destination = None @@ -779,6 +804,11 @@ class OrderParts(AjaxView): ajax_form_title = _("Order Parts") ajax_template_name = 'order/order_wizard/select_parts.html' + role_required = [ + 'part.view', + 'purchase_order.change', + ] + # List of Parts we wish to order parts = [] suppliers = [] @@ -1085,6 +1115,7 @@ class POLineItemCreate(AjaxCreateView): context_object_name = 'line' form_class = order_forms.EditPurchaseOrderLineItemForm ajax_form_title = _('Add Line Item') + role_required = 'purchase_order.add' def post(self, request, *arg, **kwargs): @@ -1199,6 +1230,7 @@ class SOLineItemCreate(AjaxCreateView): context_order_name = 'line' form_class = order_forms.EditSalesOrderLineItemForm ajax_form_title = _('Add Line Item') + role_required = 'sales_order.add' def get_form(self, *args, **kwargs): @@ -1250,6 +1282,7 @@ class SOLineItemEdit(AjaxUpdateView): model = SalesOrderLineItem form_class = order_forms.EditSalesOrderLineItemForm ajax_form_title = _('Edit Line Item') + role_required = 'sales_order.change' def get_form(self): form = super().get_form() @@ -1268,6 +1301,7 @@ class POLineItemEdit(AjaxUpdateView): form_class = order_forms.EditPurchaseOrderLineItemForm ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit Line Item') + role_required = 'purchase_order.change' def get_form(self): form = super().get_form() @@ -1285,7 +1319,8 @@ class POLineItemDelete(AjaxDeleteView): model = PurchaseOrderLineItem ajax_form_title = _('Delete Line Item') ajax_template_name = 'order/po_lineitem_delete.html' - + role_required = 'purchase_order.delete' + def get_data(self): return { 'danger': _('Deleted line item'), @@ -1297,6 +1332,7 @@ class SOLineItemDelete(AjaxDeleteView): model = SalesOrderLineItem ajax_form_title = _("Delete Line Item") ajax_template_name = "order/so_lineitem_delete.html" + role_required = 'sales_order.delete' def get_data(self): return { @@ -1310,6 +1346,7 @@ class SalesOrderAllocationCreate(AjaxCreateView): model = SalesOrderAllocation form_class = order_forms.EditSalesOrderAllocationForm ajax_form_title = _('Allocate Stock to Order') + role_required = 'sales_order.add' def get_initial(self): initials = super().get_initial().copy() @@ -1379,6 +1416,7 @@ class SalesOrderAllocationEdit(AjaxUpdateView): model = SalesOrderAllocation form_class = order_forms.EditSalesOrderAllocationForm ajax_form_title = _('Edit Allocation Quantity') + role_required = 'sales_order.change' def get_form(self): form = super().get_form() @@ -1396,3 +1434,4 @@ class SalesOrderAllocationDelete(AjaxDeleteView): ajax_form_title = _("Remove allocation") context_object_name = 'allocation' ajax_template_name = "order/so_allocation_delete.html" + role_required = 'sales_order.delete' diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index fcf137bb01..d3c713d07d 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -36,6 +36,10 @@ class RuleSet(models.Model): choice[0] for choice in RULESET_CHOICES ] + RULESET_PERMISSIONS = [ + 'view', 'add', 'change', 'delete', + ] + RULESET_MODELS = { 'admin': [ 'auth_group', From 9abb211cdf35331e5f525d9a70ec9cf3af794148 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 20:09:55 +1100 Subject: [PATCH 74/77] Update template permissions --- .../order/templates/order/order_base.html | 18 ++++++++++-------- .../order/templates/order/order_notes.html | 2 ++ .../order/templates/order/po_attachments.html | 1 - .../templates/order/purchase_order_detail.html | 6 +++--- .../order/templates/order/purchase_orders.html | 2 ++ .../templates/order/sales_order_base.html | 8 +++++--- .../order/templates/order/sales_orders.html | 2 ++ 7 files changed, 24 insertions(+), 15 deletions(-) diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 71e3455722..3b199c1840 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -32,29 +32,31 @@ src="{% static 'img/blank_image.png' %}" <p> <div class='btn-row'> <div class='btn-group action-buttons'> - <button type='button' class='btn btn-default' id='edit-order' title='Edit order information'> + {% if roles.purchase_order.change %} + <button type='button' class='btn btn-default' id='edit-order' title='{% trans "Edit order information" %}'> <span class='fas fa-edit icon-green'></span> </button> - <button type='button' class='btn btn-default' id='export-order' title='Export order to file'> - <span class='fas fa-file-download'></span> - </button> {% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %} - <button type='button' class='btn btn-default' id='place-order' title='Place order'> + <button type='button' class='btn btn-default' id='place-order' title='{% trans "Place order" %}'> <span class='fas fa-paper-plane icon-blue'></span> </button> {% elif order.status == PurchaseOrderStatus.PLACED %} - <button type='button' class='btn btn-default' id='receive-order' title='Receive items'> + <button type='button' class='btn btn-default' id='receive-order' title='{% trans "Receive items" %}'> <span class='fas fa-clipboard-check'></span> </button> - <button type='button' class='btn btn-default' id='complete-order' title='Mark order as complete'> + <button type='button' class='btn btn-default' id='complete-order' title='{% trans "Mark order as complete" %}'> <span class='fas fa-check-circle'></span> </button> {% endif %} {% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.PLACED %} - <button type='button' class='btn btn-default' id='cancel-order' title='Cancel order'> + <button type='button' class='btn btn-default' id='cancel-order' title='{% trans "Cancel order" %}'> <span class='fas fa-times-circle icon-red'></span> </button> {% endif %} + {% endif %} + <button type='button' class='btn btn-default' id='export-order' title='{% trans "Export order to file" %}'> + <span class='fas fa-file-download'></span> + </button> </div> </div> </p> diff --git a/InvenTree/order/templates/order/order_notes.html b/InvenTree/order/templates/order/order_notes.html index 237098e10d..0b45a5da4b 100644 --- a/InvenTree/order/templates/order/order_notes.html +++ b/InvenTree/order/templates/order/order_notes.html @@ -28,9 +28,11 @@ <div class='col-sm-6'> <h4>{% trans "Order Notes" %}</h4> </div> + {% if roles.purchase_order.change %} <div class='col-sm-6'> <button title='{% trans "Edit notes" %}' class='btn btn-default action-button float-right' id='edit-notes'><span class='fas fa-edit'></span></button> </div> + {% endif %} </div> <hr> <div class='panel panel-default'> diff --git a/InvenTree/order/templates/order/po_attachments.html b/InvenTree/order/templates/order/po_attachments.html index f5421e8760..d60d605771 100644 --- a/InvenTree/order/templates/order/po_attachments.html +++ b/InvenTree/order/templates/order/po_attachments.html @@ -14,7 +14,6 @@ {% include "attachment_table.html" with attachments=order.attachments.all %} - {% endblock %} {% block js_ready %} diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 9b9eddb887..642ff96d4a 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -12,7 +12,7 @@ <hr> <div id='order-toolbar-buttons' class='btn-group' style='float: right;'> - {% if order.status == PurchaseOrderStatus.PENDING %} + {% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %} <button type='button' class='btn btn-default' id='new-po-line'>{% trans "Add Line Item" %}</button> {% endif %} </div> @@ -209,12 +209,12 @@ $("#po-table").inventreeTable({ var pk = row.pk; - {% if order.status == PurchaseOrderStatus.PENDING %} + {% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.delete %} html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}'); html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}'); {% endif %} - {% if order.status == PurchaseOrderStatus.PLACED %} + {% if order.status == PurchaseOrderStatus.PLACED and roles.purchase_order.change %} if (row.received < row.quantity) { html += makeIconButton('fa-clipboard-check', 'button-line-receive', pk, '{% trans "Receive line item" %}'); } diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index 1019092151..d02af36ff5 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -14,7 +14,9 @@ InvenTree | {% trans "Purchase Orders" %} <div id='table-buttons'> <div class='button-toolbar container-fluid' style='float: right;'> + {% if roles.purchase_order.add %} <button class='btn btn-primary' type='button' id='po-create' title='{% trans "Create new purchase order" %}'>{% trans "New Purchase Order" %}</button> + {% endif %} <div class='filter-list' id='filter-list-purchaseorder'> <!-- An empty div in which the filter list will be constructed --> </div> diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 52856cadbd..2eb13eaa64 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -41,12 +41,10 @@ src="{% static 'img/blank_image.png' %}" <p>{{ order.description }}</p> <div class='btn-row'> <div class='btn-group action-buttons'> + {% if roles.sales_order.change %} <button type='button' class='btn btn-default' id='edit-order' title='Edit order information'> <span class='fas fa-edit icon-green'></span> </button> - <button type='button' disabled='' class='btn btn-default' id='packing-list' title='{% trans "Packing List" %}'> - <span class='fas fa-clipboard-list'></span> - </button> {% if order.status == SalesOrderStatus.PENDING %} <button type='button' class='btn btn-default' id='ship-order' title='{% trans "Ship order" %}'> <span class='fas fa-paper-plane icon-blue'></span> @@ -55,6 +53,10 @@ src="{% static 'img/blank_image.png' %}" <span class='fas fa-times-circle icon-red'></span> </button> {% endif %} + {% endif %} + <button type='button' disabled='' class='btn btn-default' id='packing-list' title='{% trans "Packing List" %}'> + <span class='fas fa-clipboard-list'></span> + </button> </div> </div> {% endblock %} diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html index 4e29156773..dfe09d5d0d 100644 --- a/InvenTree/order/templates/order/sales_orders.html +++ b/InvenTree/order/templates/order/sales_orders.html @@ -14,7 +14,9 @@ InvenTree | {% trans "Sales Orders" %} <div id='table-buttons'> <div class='button-toolbar container-fluid' style='float: right;'> + {% if roles.sales_order.add %} <button class='btn btn-primary' type='button' id='so-create' title='{% trans "Create new sales order" %}'>{% trans "New Sales Order" %}</button> + {% endif %} <div class='filter-list' id='filter-list-salesorder'> <!-- An empty div in which the filter list will be constructed --> </div> From 2325b1e4bad9a0fb21449992831337f2c7bc12c5 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 20:10:14 +1100 Subject: [PATCH 75/77] Unit test fixes --- InvenTree/order/test_views.py | 17 ++++++++++++++++- InvenTree/order/views.py | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index 932cac9060..246cc2dd48 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group from InvenTree.status_codes import PurchaseOrderStatus @@ -32,7 +33,21 @@ class OrderViewTestCase(TestCase): # Create a user User = get_user_model() - User.objects.create_user('username', 'user@email.com', 'password') + user = User.objects.create_user('username', 'user@email.com', 'password') + + # Ensure that the user has the correct permissions! + g = Group.objects.create(name='orders') + user.groups.add(g) + + for rule in g.rule_sets.all(): + if rule.name in ['purchase_order', 'sales_order']: + rule.can_change = True + rule.can_add = True + rule.can_delete = True + + rule.save() + + g.save() self.client.login(username='username', password='password') diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 28b8416b65..d2d02bb2c9 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -255,7 +255,7 @@ class PurchaseOrderNotes(InvenTreeRoleMixin, UpdateView): context_object_name = 'order' template_name = 'order/order_notes.html' model = PurchaseOrder - role_required = 'purchase_order.change' + role_required = 'purchase_order.view' fields = ['notes'] From b80e4302baffe5cc79aedca35aaf85cc5ef61eff Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 20:29:16 +1100 Subject: [PATCH 76/77] Update permissions for build app --- .../build/templates/build/build_base.html | 12 ++--- InvenTree/build/tests.py | 45 ++++++++++++++++++- InvenTree/build/views.py | 18 +++++++- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index ed3da576d5..f076013def 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -41,19 +41,21 @@ src="{% static 'img/blank_image.png' %}" </h4> <div class='btn-row'> <div class='btn-group action-buttons'> - <button type='button' class='btn btn-default' id='build-edit' title='Edit Build'> + {% if roles.build.change %} + <button type='button' class='btn btn-default' id='build-edit' title='{% trans "Edit Build" %}'> <span class='fas fa-edit icon-green'/> </button> {% if build.is_active %} - <button type='button' class='btn btn-default' id='build-complete' title="Complete Build"> + <button type='button' class='btn btn-default' id='build-complete' title='{% trans "Complete Build" %}'> <span class='fas fa-tools'/> </button> - <button type='button' class='btn btn-default btn-glyph' id='build-cancel' title='Cancel Build'> + <button type='button' class='btn btn-default btn-glyph' id='build-cancel' title='{% trans "Cancel Build" %}'> <span class='fas fa-times-circle icon-red'/> </button> {% endif %} - {% if build.status == BuildStatus.CANCELLED %} - <button type='button' class='btn btn-default btn-glyph' id='build-delete' title='Delete Build'> + {% endif %} + {% if build.status == BuildStatus.CANCELLED and roles.build.delete %} + <button type='button' class='btn btn-default btn-glyph' id='build-delete' title='{% trans "Delete Build" %}'> <span class='fas fa-trash-alt icon-red'/> </button> {% endif %} diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 9b6d51c33a..92ca034a4a 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group from rest_framework.test import APITestCase from rest_framework import status @@ -30,6 +31,20 @@ class BuildTestSimple(TestCase): User.objects.create_user('testuser', 'test@testing.com', 'password') self.user = User.objects.get(username='testuser') + + g = Group.objects.create(name='builders') + self.user.groups.add(g) + + for rule in g.rule_sets.all(): + if rule.name == 'build': + rule.can_change = True + rule.can_add = True + rule.can_delete = True + + rule.save() + + g.save() + self.client.login(username='testuser', password='password') def test_build_objects(self): @@ -94,7 +109,20 @@ class TestBuildAPI(APITestCase): def setUp(self): # Create a user for auth User = get_user_model() - User.objects.create_user('testuser', 'test@testing.com', 'password') + user = User.objects.create_user('testuser', 'test@testing.com', 'password') + + g = Group.objects.create(name='builders') + user.groups.add(g) + + for rule in g.rule_sets.all(): + if rule.name == 'build': + rule.can_change = True + rule.can_add = True + rule.can_delete = True + + rule.save() + + g.save() self.client.login(username='testuser', password='password') @@ -131,7 +159,20 @@ class TestBuildViews(TestCase): # Create a user User = get_user_model() - User.objects.create_user('username', 'user@email.com', 'password') + user = User.objects.create_user('username', 'user@email.com', 'password') + + g = Group.objects.create(name='builders') + user.groups.add(g) + + for rule in g.rule_sets.all(): + if rule.name == 'build': + rule.can_change = True + rule.can_add = True + rule.can_delete = True + + rule.save() + + g.save() self.client.login(username='username', password='password') diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 88dc66085f..b2b8b24502 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -17,16 +17,18 @@ from . import forms from stock.models import StockLocation, StockItem from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView +from InvenTree.views import InvenTreeRoleMixin from InvenTree.helpers import str2bool, ExtractSerialNumbers from InvenTree.status_codes import BuildStatus -class BuildIndex(ListView): +class BuildIndex(InvenTreeRoleMixin, ListView): """ View for displaying list of Builds """ model = Build template_name = 'build/index.html' context_object_name = 'builds' + role_required = 'build.view' def get_queryset(self): """ Return all Build objects (order by date, newest first) """ @@ -56,6 +58,7 @@ class BuildCancel(AjaxUpdateView): ajax_form_title = _('Cancel Build') context_object_name = 'build' form_class = forms.CancelBuildForm + role_required = 'build.change' def post(self, request, *args, **kwargs): """ Handle POST request. Mark the build status as CANCELLED """ @@ -94,6 +97,7 @@ class BuildAutoAllocate(AjaxUpdateView): context_object_name = 'build' ajax_form_title = _('Allocate Stock') ajax_template_name = 'build/auto_allocate.html' + role_required = 'build.change' def get_context_data(self, *args, **kwargs): """ Get the context data for form rendering. """ @@ -147,6 +151,7 @@ class BuildUnallocate(AjaxUpdateView): form_class = forms.ConfirmBuildForm ajax_form_title = _("Unallocate Stock") ajax_template_name = "build/unallocate.html" + form_required = 'build.change' def post(self, request, *args, **kwargs): @@ -184,6 +189,7 @@ class BuildComplete(AjaxUpdateView): context_object_name = "build" ajax_form_title = _("Complete Build") ajax_template_name = "build/complete.html" + role_required = 'build.change' def get_form(self): """ Get the form object. @@ -325,6 +331,7 @@ class BuildNotes(UpdateView): context_object_name = 'build' template_name = 'build/notes.html' model = Build + role_required = 'build.view' fields = ['notes'] @@ -342,9 +349,11 @@ class BuildNotes(UpdateView): class BuildDetail(DetailView): """ Detail view of a single Build object. """ + model = Build template_name = 'build/detail.html' context_object_name = 'build' + role_required = 'build.view' def get_context_data(self, **kwargs): @@ -363,6 +372,7 @@ class BuildAllocate(DetailView): model = Build context_object_name = 'build' template_name = 'build/allocate.html' + role_required = ['build.change'] def get_context_data(self, **kwargs): """ Provide extra context information for the Build allocation page """ @@ -392,6 +402,7 @@ class BuildCreate(AjaxCreateView): form_class = forms.EditBuildForm ajax_form_title = _('Start new Build') ajax_template_name = 'modal_form.html' + role_required = 'build.add' def get_initial(self): """ Get initial parameters for Build creation. @@ -427,6 +438,7 @@ class BuildUpdate(AjaxUpdateView): context_object_name = 'build' ajax_form_title = _('Edit Build Details') ajax_template_name = 'modal_form.html' + role_required = 'build.change' def get_data(self): return { @@ -440,6 +452,7 @@ class BuildDelete(AjaxDeleteView): model = Build ajax_template_name = 'build/delete_build.html' ajax_form_title = _('Delete Build') + role_required = 'build.delete' class BuildItemDelete(AjaxDeleteView): @@ -451,6 +464,7 @@ class BuildItemDelete(AjaxDeleteView): ajax_template_name = 'build/delete_build_item.html' ajax_form_title = _('Unallocate Stock') context_object_name = 'item' + role_required = 'build.delete' def get_data(self): return { @@ -465,6 +479,7 @@ class BuildItemCreate(AjaxCreateView): form_class = forms.EditBuildItemForm ajax_template_name = 'build/create_build_item.html' ajax_form_title = _('Allocate new Part') + role_required = 'build.add' part = None available_stock = None @@ -618,6 +633,7 @@ class BuildItemEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' form_class = forms.EditBuildItemForm ajax_form_title = _('Edit Stock Allocation') + role_required = 'build.change' def get_data(self): return { From 8b37229e4b3b84c5e241f5e4418e2f1eaee3fc29 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 6 Oct 2020 20:31:38 +1100 Subject: [PATCH 77/77] The real translations are the ones we made along the way --- InvenTree/locale/de/LC_MESSAGES/django.po | 442 ++++++++++++---------- InvenTree/locale/en/LC_MESSAGES/django.po | 416 ++++++++++---------- InvenTree/locale/es/LC_MESSAGES/django.po | 416 ++++++++++---------- 3 files changed, 668 insertions(+), 606 deletions(-) diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po index 7632fadc4c..7f955ffea2 100644 --- a/InvenTree/locale/de/LC_MESSAGES/django.po +++ b/InvenTree/locale/de/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-05 13:20+0000\n" +"POT-Creation-Date: 2020-10-06 09:31+0000\n" "PO-Revision-Date: 2020-05-03 11:32+0200\n" "Last-Translator: Christian Schlüter <chschlue@gmail.com>\n" "Language-Team: C <kde-i18n-doc@kde.org>\n" @@ -17,11 +17,11 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Lokalize 19.12.0\n" -#: InvenTree/api.py:83 +#: InvenTree/api.py:85 msgid "No action specified" msgstr "Keine Aktion angegeben" -#: InvenTree/api.py:97 +#: InvenTree/api.py:99 msgid "No matching action found" msgstr "Keine passende Aktion gefunden" @@ -49,35 +49,35 @@ msgstr "" msgid "Apply Theme" msgstr "" -#: InvenTree/helpers.py:337 order/models.py:187 order/models.py:261 +#: InvenTree/helpers.py:339 order/models.py:187 order/models.py:261 msgid "Invalid quantity provided" msgstr "Keine gültige Menge" -#: InvenTree/helpers.py:340 +#: InvenTree/helpers.py:342 msgid "Empty serial number string" msgstr "Keine Seriennummer angegeben" -#: InvenTree/helpers.py:361 +#: InvenTree/helpers.py:363 #, python-brace-format msgid "Duplicate serial: {n}" msgstr "Doppelte Seriennummer: {n}" -#: InvenTree/helpers.py:365 InvenTree/helpers.py:368 InvenTree/helpers.py:371 +#: InvenTree/helpers.py:367 InvenTree/helpers.py:370 InvenTree/helpers.py:373 #, python-brace-format msgid "Invalid group: {g}" msgstr "Ungültige Gruppe: {g}" -#: InvenTree/helpers.py:376 +#: InvenTree/helpers.py:378 #, fuzzy, python-brace-format #| msgid "Duplicate serial: {n}" msgid "Duplicate serial: {g}" msgstr "Doppelte Seriennummer: {n}" -#: InvenTree/helpers.py:384 +#: InvenTree/helpers.py:386 msgid "No serial numbers found" msgstr "Keine Seriennummern gefunden" -#: InvenTree/helpers.py:388 +#: InvenTree/helpers.py:390 #, python-brace-format msgid "Number of unique serial number ({s}) must match quantity ({q})" msgstr "" @@ -107,19 +107,19 @@ msgstr "Name" msgid "Description (optional)" msgstr "Firmenbeschreibung" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:348 msgid "English" msgstr "Englisch" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:349 msgid "German" msgstr "Deutsch" -#: InvenTree/settings.py:345 +#: InvenTree/settings.py:350 msgid "French" msgstr "Französisch" -#: InvenTree/settings.py:346 +#: InvenTree/settings.py:351 msgid "Polish" msgstr "Polnisch" @@ -152,7 +152,7 @@ msgid "Returned" msgstr "Zurückgegeben" #: InvenTree/status_codes.py:136 -#: order/templates/order/sales_order_base.html:103 +#: order/templates/order/sales_order_base.html:105 msgid "Shipped" msgstr "Versendet" @@ -207,7 +207,7 @@ msgstr "Überschuss darf 100% nicht überschreiten" msgid "Overage must be an integer value or a percentage" msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" -#: InvenTree/views.py:661 +#: InvenTree/views.py:703 msgid "Database Statistics" msgstr "Datenbankstatistiken" @@ -281,7 +281,7 @@ msgstr "Bau-Fertigstellung bestätigen" msgid "Build quantity must be integer value for trackable parts" msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" -#: build/models.py:73 build/templates/build/build_base.html:70 +#: build/models.py:73 build/templates/build/build_base.html:72 msgid "Build Title" msgstr "Bau-Titel" @@ -289,7 +289,7 @@ msgstr "Bau-Titel" msgid "Brief description of the build" msgstr "Kurze Beschreibung des Baus" -#: build/models.py:84 build/templates/build/build_base.html:91 +#: build/models.py:84 build/templates/build/build_base.html:93 msgid "Parent Build" msgstr "Eltern-Bau" @@ -299,7 +299,7 @@ msgstr "Eltern-Bau, dem dieser Bau zugewiesen ist" #: build/models.py:90 build/templates/build/allocate.html:329 #: build/templates/build/auto_allocate.html:19 -#: build/templates/build/build_base.html:75 +#: build/templates/build/build_base.html:77 #: build/templates/build/detail.html:22 order/models.py:501 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:147 @@ -424,7 +424,7 @@ msgid "Stock quantity to allocate to build" msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" #: build/templates/build/allocate.html:17 -#: company/templates/company/detail_part.html:18 order/views.py:779 +#: company/templates/company/detail_part.html:18 order/views.py:804 #: part/templates/part/category.html:122 msgid "Order Parts" msgstr "Teile bestellen" @@ -458,7 +458,7 @@ msgstr "Seriennummer" #: build/templates/build/allocate.html:172 #: build/templates/build/auto_allocate.html:20 -#: build/templates/build/build_base.html:80 +#: build/templates/build/build_base.html:82 #: build/templates/build/detail.html:27 #: company/templates/company/supplier_part_pricing.html:71 #: order/templates/order/order_wizard/select_parts.html:32 @@ -598,11 +598,29 @@ msgstr "Dieser Bau ist Kind von Bau" msgid "Admin view" msgstr "Admin" -#: build/templates/build/build_base.html:66 build/templates/build/detail.html:9 +#: build/templates/build/build_base.html:45 +#, fuzzy +#| msgid "Edited build" +msgid "Edit Build" +msgstr "Bau bearbeitet" + +#: build/templates/build/build_base.html:49 build/views.py:190 +msgid "Complete Build" +msgstr "Bau fertigstellen" + +#: build/templates/build/build_base.html:52 build/views.py:58 +msgid "Cancel Build" +msgstr "Bau abbrechen" + +#: build/templates/build/build_base.html:58 build/views.py:454 +msgid "Delete Build" +msgstr "Bau entfernt" + +#: build/templates/build/build_base.html:68 build/templates/build/detail.html:9 msgid "Build Details" msgstr "Bau-Status" -#: build/templates/build/build_base.html:85 +#: build/templates/build/build_base.html:87 #: build/templates/build/detail.html:42 #: order/templates/order/receive_parts.html:24 #: stock/templates/stock/item_base.html:276 templates/InvenTree/search.html:175 @@ -612,7 +630,7 @@ msgstr "Bau-Status" msgid "Status" msgstr "Status" -#: build/templates/build/build_base.html:98 order/models.py:499 +#: build/templates/build/build_base.html:100 order/models.py:499 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 @@ -622,15 +640,15 @@ msgstr "Status" msgid "Sales Order" msgstr "Bestellung" -#: build/templates/build/build_base.html:104 +#: build/templates/build/build_base.html:106 msgid "BOM Price" msgstr "Stücklistenpreis" -#: build/templates/build/build_base.html:109 +#: build/templates/build/build_base.html:111 msgid "BOM pricing is incomplete" msgstr "Stücklistenbepreisung ist unvollständig" -#: build/templates/build/build_base.html:112 +#: build/templates/build/build_base.html:114 msgid "No pricing information" msgstr "Keine Preisinformation" @@ -694,8 +712,8 @@ msgid "Batch" msgstr "Los" #: build/templates/build/detail.html:61 -#: order/templates/order/order_base.html:98 -#: order/templates/order/sales_order_base.html:97 templates/js/build.html:71 +#: order/templates/order/order_base.html:100 +#: order/templates/order/sales_order_base.html:99 templates/js/build.html:71 msgid "Created" msgstr "Erstellt" @@ -737,7 +755,7 @@ msgid "Save" msgstr "Speichern" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 -#: order/templates/order/order_notes.html:32 +#: order/templates/order/order_notes.html:33 #: order/templates/order/sales_order_notes.html:37 #: part/templates/part/notes.html:33 stock/templates/stock/item_notes.html:33 msgid "Edit notes" @@ -757,100 +775,88 @@ msgid "Are you sure you wish to unallocate all stock for this build?" msgstr "" "Sind Sie sicher, dass sie alle Lagerobjekte von diesem Bau entfernen möchten?" -#: build/views.py:56 -msgid "Cancel Build" -msgstr "Bau abbrechen" - -#: build/views.py:74 +#: build/views.py:77 msgid "Confirm build cancellation" msgstr "Bauabbruch bestätigen" -#: build/views.py:79 +#: build/views.py:82 msgid "Build was cancelled" msgstr "Bau wurde abgebrochen" -#: build/views.py:95 +#: build/views.py:98 msgid "Allocate Stock" msgstr "Lagerbestand zuweisen" -#: build/views.py:108 +#: build/views.py:112 msgid "No matching build found" msgstr "Kein passender Bau gefunden" -#: build/views.py:127 +#: build/views.py:131 msgid "Confirm stock allocation" msgstr "Lagerbestandszuordnung bestätigen" -#: build/views.py:128 +#: build/views.py:132 msgid "Check the confirmation box at the bottom of the list" msgstr "Bestätigunsbox am Ende der Liste bestätigen" -#: build/views.py:148 build/views.py:452 +#: build/views.py:152 build/views.py:465 msgid "Unallocate Stock" msgstr "Zuweisung aufheben" -#: build/views.py:161 +#: build/views.py:166 msgid "Confirm unallocation of build stock" msgstr "Zuweisungsaufhebung bestätigen" -#: build/views.py:162 stock/views.py:405 +#: build/views.py:167 stock/views.py:405 msgid "Check the confirmation box" msgstr "Bestätigungsbox bestätigen" -#: build/views.py:185 -msgid "Complete Build" -msgstr "Bau fertigstellen" - -#: build/views.py:264 +#: build/views.py:270 msgid "Confirm completion of build" msgstr "Baufertigstellung bestätigen" -#: build/views.py:271 +#: build/views.py:277 msgid "Invalid location selected" msgstr "Ungültige Ortsauswahl" -#: build/views.py:296 stock/views.py:1621 +#: build/views.py:302 stock/views.py:1621 #, python-brace-format msgid "The following serial numbers already exist: ({sn})" msgstr "Die folgende Seriennummer existiert bereits: ({sn})" -#: build/views.py:317 +#: build/views.py:323 msgid "Build marked as COMPLETE" msgstr "Bau als FERTIG markiert" -#: build/views.py:393 +#: build/views.py:403 msgid "Start new Build" msgstr "Neuen Bau beginnen" -#: build/views.py:418 +#: build/views.py:429 msgid "Created new build" msgstr "Neuen Bau angelegt" -#: build/views.py:428 +#: build/views.py:439 msgid "Edit Build Details" msgstr "Baudetails bearbeiten" -#: build/views.py:433 +#: build/views.py:445 msgid "Edited build" msgstr "Bau bearbeitet" -#: build/views.py:442 -msgid "Delete Build" -msgstr "Bau entfernt" - -#: build/views.py:457 +#: build/views.py:471 msgid "Removed parts from build allocation" msgstr "Teile von Bauzuordnung entfernt" -#: build/views.py:467 +#: build/views.py:481 msgid "Allocate new Part" msgstr "Neues Teil zuordnen" -#: build/views.py:620 +#: build/views.py:635 msgid "Edit Stock Allocation" msgstr "Teilzuordnung bearbeiten" -#: build/views.py:624 +#: build/views.py:640 msgid "Updated Build Item" msgstr "Bauobjekt aktualisiert" @@ -1055,7 +1061,7 @@ msgstr "Hersteller" #: company/templates/company/detail.html:21 #: company/templates/company/supplier_part_base.html:66 #: company/templates/company/supplier_part_detail.html:21 -#: order/templates/order/order_base.html:79 +#: order/templates/order/order_base.html:81 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 #: stock/templates/stock/item_base.html:251 templates/js/company.html:48 #: templates/js/company.html:162 templates/js/order.html:146 @@ -1063,7 +1069,7 @@ msgid "Supplier" msgstr "Zulieferer" #: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:78 stock/models.py:370 +#: order/templates/order/sales_order_base.html:80 stock/models.py:370 #: stock/models.py:371 stock/templates/stock/item_base.html:169 #: templates/js/company.html:40 templates/js/order.html:221 msgid "Customer" @@ -1169,12 +1175,12 @@ msgid "Purchase Orders" msgstr "Bestellungen" #: company/templates/company/purchase_orders.html:14 -#: order/templates/order/purchase_orders.html:17 +#: order/templates/order/purchase_orders.html:18 msgid "Create new purchase order" msgstr "Neue Bestellung anlegen" #: company/templates/company/purchase_orders.html:14 -#: order/templates/order/purchase_orders.html:17 +#: order/templates/order/purchase_orders.html:18 msgid "New Purchase Order" msgstr "Neue Bestellung" @@ -1188,12 +1194,12 @@ msgid "Sales Orders" msgstr "Bestellungen" #: company/templates/company/sales_orders.html:14 -#: order/templates/order/sales_orders.html:17 +#: order/templates/order/sales_orders.html:18 msgid "Create new sales order" msgstr "Neuen Auftrag anlegen" #: company/templates/company/sales_orders.html:14 -#: order/templates/order/sales_orders.html:17 +#: order/templates/order/sales_orders.html:18 msgid "New Sales Order" msgstr "Neuer Auftrag" @@ -1251,7 +1257,7 @@ msgid "Pricing Information" msgstr "Preisinformationen ansehen" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2226 +#: part/templates/part/sale_prices.html:13 part/views.py:2228 msgid "Add Price Break" msgstr "Preisstaffel hinzufügen" @@ -1382,17 +1388,17 @@ msgstr "Neues Zuliefererteil anlegen" msgid "Delete Supplier Part" msgstr "Zuliefererteil entfernen" -#: company/views.py:404 part/views.py:2232 +#: company/views.py:404 part/views.py:2234 #, fuzzy #| msgid "Add Price Break" msgid "Added new price break" msgstr "Preisstaffel hinzufügen" -#: company/views.py:441 part/views.py:2277 +#: company/views.py:441 part/views.py:2279 msgid "Edit Price Break" msgstr "Preisstaffel bearbeiten" -#: company/views.py:456 part/views.py:2293 +#: company/views.py:456 part/views.py:2295 msgid "Delete Price Break" msgstr "Preisstaffel löschen" @@ -1424,20 +1430,20 @@ msgstr "" msgid "Enabled" msgstr "" -#: order/forms.py:24 +#: order/forms.py:24 order/templates/order/order_base.html:40 msgid "Place order" msgstr "Bestellung aufgeben" -#: order/forms.py:35 +#: order/forms.py:35 order/templates/order/order_base.html:47 msgid "Mark order as complete" msgstr "Bestellung als vollständig markieren" -#: order/forms.py:46 order/forms.py:57 -#: order/templates/order/sales_order_base.html:54 +#: order/forms.py:46 order/forms.py:57 order/templates/order/order_base.html:52 +#: order/templates/order/sales_order_base.html:52 msgid "Cancel order" msgstr "Bestellung stornieren" -#: order/forms.py:68 order/templates/order/sales_order_base.html:51 +#: order/forms.py:68 order/templates/order/sales_order_base.html:49 msgid "Ship order" msgstr "Bestellung versenden" @@ -1497,7 +1503,7 @@ msgstr "" msgid "Date order was completed" msgstr "Bestellung als vollständig markieren" -#: order/models.py:185 order/models.py:259 part/views.py:1343 +#: order/models.py:185 order/models.py:259 part/views.py:1345 #: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "Anzahl muss größer Null sein" @@ -1578,32 +1584,48 @@ msgstr "Zuordnungsanzahl eingeben" msgid "Are you sure you want to delete this attachment?" msgstr "Sind Sie sicher, dass Sie diesen Anhang löschen wollen?" -#: order/templates/order/order_base.html:64 +#: order/templates/order/order_base.html:36 +#, fuzzy +#| msgid "Edited company information" +msgid "Edit order information" +msgstr "Firmeninformation bearbeitet" + +#: order/templates/order/order_base.html:44 +#, fuzzy +#| msgid "Receive line item" +msgid "Receive items" +msgstr "Position empfangen" + +#: order/templates/order/order_base.html:57 +msgid "Export order to file" +msgstr "" + +#: order/templates/order/order_base.html:66 msgid "Purchase Order Details" msgstr "Bestelldetails" -#: order/templates/order/order_base.html:69 -#: order/templates/order/sales_order_base.html:68 +#: order/templates/order/order_base.html:71 +#: order/templates/order/sales_order_base.html:70 msgid "Order Reference" msgstr "Bestellreferenz" -#: order/templates/order/order_base.html:74 -#: order/templates/order/sales_order_base.html:73 +#: order/templates/order/order_base.html:76 +#: order/templates/order/sales_order_base.html:75 msgid "Order Status" msgstr "Bestellstatus" -#: order/templates/order/order_base.html:85 templates/js/order.html:153 +#: order/templates/order/order_base.html:87 templates/js/order.html:153 msgid "Supplier Reference" msgstr "Zuliefererreferenz" -#: order/templates/order/order_base.html:104 +#: order/templates/order/order_base.html:106 msgid "Issued" msgstr "Aufgegeben" -#: order/templates/order/order_base.html:111 +#: order/templates/order/order_base.html:113 #: order/templates/order/purchase_order_detail.html:182 #: order/templates/order/receive_parts.html:22 -#: order/templates/order/sales_order_base.html:110 +#: order/templates/order/sales_order_base.html:112 msgid "Received" msgstr "Empfangen" @@ -1672,8 +1694,8 @@ msgid "Attachments" msgstr "Anhänge" #: order/templates/order/purchase_order_detail.html:16 -#: order/templates/order/sales_order_detail.html:17 order/views.py:1087 -#: order/views.py:1201 +#: order/templates/order/sales_order_detail.html:17 order/views.py:1117 +#: order/views.py:1232 msgid "Add Line Item" msgstr "Position hinzufügen" @@ -1743,15 +1765,15 @@ msgstr "" msgid "This SalesOrder has not been fully allocated" msgstr "Dieser Auftrag ist nicht vollständig zugeordnet" -#: order/templates/order/sales_order_base.html:47 +#: order/templates/order/sales_order_base.html:57 msgid "Packing List" msgstr "Packliste" -#: order/templates/order/sales_order_base.html:63 +#: order/templates/order/sales_order_base.html:65 msgid "Sales Order Details" msgstr "Auftragsdetails" -#: order/templates/order/sales_order_base.html:84 templates/js/order.html:228 +#: order/templates/order/sales_order_base.html:86 templates/js/order.html:228 msgid "Customer Reference" msgstr "Kundenreferenz" @@ -1819,147 +1841,147 @@ msgstr "Sind Sie sicher, dass Sie diese Position löschen möchten?" msgid "Order Items" msgstr "Bestellungspositionen" -#: order/views.py:93 +#: order/views.py:99 msgid "Add Purchase Order Attachment" msgstr "Bestellanhang hinzufügen" -#: order/views.py:102 order/views.py:149 part/views.py:90 stock/views.py:167 +#: order/views.py:109 order/views.py:157 part/views.py:92 stock/views.py:167 msgid "Added attachment" msgstr "Anhang hinzugefügt" -#: order/views.py:141 +#: order/views.py:148 msgid "Add Sales Order Attachment" msgstr "Auftragsanhang hinzufügen" -#: order/views.py:176 order/views.py:197 +#: order/views.py:184 order/views.py:206 msgid "Edit Attachment" msgstr "Anhang bearbeiten" -#: order/views.py:180 order/views.py:201 +#: order/views.py:189 order/views.py:211 msgid "Attachment updated" msgstr "Anhang aktualisiert" -#: order/views.py:216 order/views.py:230 +#: order/views.py:226 order/views.py:241 msgid "Delete Attachment" msgstr "Anhang löschen" -#: order/views.py:222 order/views.py:236 stock/views.py:223 +#: order/views.py:233 order/views.py:248 stock/views.py:223 msgid "Deleted attachment" msgstr "Anhang gelöscht" -#: order/views.py:287 +#: order/views.py:301 msgid "Create Purchase Order" msgstr "Bestellung anlegen" -#: order/views.py:318 +#: order/views.py:333 msgid "Create Sales Order" msgstr "Auftrag anlegen" -#: order/views.py:348 +#: order/views.py:364 msgid "Edit Purchase Order" msgstr "Bestellung bearbeiten" -#: order/views.py:368 +#: order/views.py:385 msgid "Edit Sales Order" msgstr "Auftrag bearbeiten" -#: order/views.py:384 +#: order/views.py:402 msgid "Cancel Order" msgstr "Bestellung stornieren" -#: order/views.py:399 order/views.py:431 +#: order/views.py:418 order/views.py:451 msgid "Confirm order cancellation" msgstr "Bestellstornierung bestätigen" -#: order/views.py:417 +#: order/views.py:436 msgid "Cancel sales order" msgstr "Auftrag stornieren" -#: order/views.py:437 +#: order/views.py:457 msgid "Could not cancel order" msgstr "Stornierung fehlgeschlagen" -#: order/views.py:451 +#: order/views.py:471 msgid "Issue Order" msgstr "Bestellung aufgeben" -#: order/views.py:466 +#: order/views.py:487 msgid "Confirm order placement" msgstr "Bestellungstätigung bestätigen" -#: order/views.py:487 +#: order/views.py:508 msgid "Complete Order" msgstr "Auftrag fertigstellen" -#: order/views.py:522 +#: order/views.py:544 msgid "Ship Order" msgstr "Versenden" -#: order/views.py:538 +#: order/views.py:561 msgid "Confirm order shipment" msgstr "Versand bestätigen" -#: order/views.py:544 +#: order/views.py:567 msgid "Could not ship order" msgstr "Versand fehlgeschlagen" -#: order/views.py:595 +#: order/views.py:619 msgid "Receive Parts" msgstr "Teile empfangen" -#: order/views.py:662 +#: order/views.py:687 msgid "Items received" msgstr "Anzahl empfangener Positionen" -#: order/views.py:676 +#: order/views.py:701 msgid "No destination set" msgstr "Kein Ziel gesetzt" -#: order/views.py:721 +#: order/views.py:746 msgid "Error converting quantity to number" msgstr "Fehler beim Konvertieren zu Zahl" -#: order/views.py:727 +#: order/views.py:752 msgid "Receive quantity less than zero" msgstr "Anzahl kleiner null empfangen" -#: order/views.py:733 +#: order/views.py:758 msgid "No lines specified" msgstr "Keine Zeilen angegeben" -#: order/views.py:1107 +#: order/views.py:1138 msgid "Invalid Purchase Order" msgstr "Ungültige Bestellung" -#: order/views.py:1115 +#: order/views.py:1146 msgid "Supplier must match for Part and Order" msgstr "Zulieferer muss zum Teil und zur Bestellung passen" -#: order/views.py:1120 +#: order/views.py:1151 msgid "Invalid SupplierPart selection" msgstr "Ungültige Wahl des Zulieferer-Teils" -#: order/views.py:1252 order/views.py:1270 +#: order/views.py:1284 order/views.py:1303 msgid "Edit Line Item" msgstr "Position bearbeiten" -#: order/views.py:1286 order/views.py:1298 +#: order/views.py:1320 order/views.py:1333 msgid "Delete Line Item" msgstr "Position löschen" -#: order/views.py:1291 order/views.py:1303 +#: order/views.py:1326 order/views.py:1339 msgid "Deleted line item" msgstr "Position gelöscht" -#: order/views.py:1312 +#: order/views.py:1348 msgid "Allocate Stock to Order" msgstr "Lagerbestand dem Auftrag zuweisen" -#: order/views.py:1381 +#: order/views.py:1418 msgid "Edit Allocation Quantity" msgstr "Zuordnung bearbeiten" -#: order/views.py:1396 +#: order/views.py:1434 msgid "Remove allocation" msgstr "Zuordnung entfernen" @@ -2339,7 +2361,7 @@ msgstr "Notizen zum Stücklisten-Objekt" msgid "BOM line checksum" msgstr "Prüfsumme der Stückliste" -#: part/models.py:1612 part/views.py:1349 part/views.py:1401 +#: part/models.py:1612 part/views.py:1351 part/views.py:1403 #: stock/models.py:231 #, fuzzy #| msgid "Overage must be an integer value or a percentage" @@ -2410,7 +2432,7 @@ msgstr "Stückliste bearbeiten" msgid "Validate Bill of Materials" msgstr "Stückliste validieren" -#: part/templates/part/bom.html:48 part/views.py:1640 +#: part/templates/part/bom.html:48 part/views.py:1642 msgid "Export Bill of Materials" msgstr "Stückliste exportieren" @@ -2526,7 +2548,7 @@ msgstr "Neuen Bau beginnen" msgid "All parts" msgstr "Alle Teile" -#: part/templates/part/category.html:24 part/views.py:2043 +#: part/templates/part/category.html:24 part/views.py:2045 msgid "Create new part category" msgstr "Teilkategorie anlegen" @@ -2570,7 +2592,7 @@ msgstr "Teile (inklusive Unter-Kategorien)" msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:114 part/views.py:511 +#: part/templates/part/category.html:114 part/views.py:513 msgid "Create new part" msgstr "Neues Teil anlegen" @@ -2815,7 +2837,7 @@ msgid "Edit" msgstr "Bearbeiten" #: part/templates/part/params.html:39 part/templates/part/supplier.html:17 -#: users/models.py:141 +#: users/models.py:145 msgid "Delete" msgstr "Löschen" @@ -3051,184 +3073,184 @@ msgstr "Neues Teil hinzufügen" msgid "New Variant" msgstr "Varianten" -#: part/views.py:78 +#: part/views.py:80 msgid "Add part attachment" msgstr "Teilanhang hinzufügen" -#: part/views.py:129 templates/attachment_table.html:30 +#: part/views.py:131 templates/attachment_table.html:30 msgid "Edit attachment" msgstr "Anhang bearbeiten" -#: part/views.py:135 +#: part/views.py:137 msgid "Part attachment updated" msgstr "Teilanhang aktualisiert" -#: part/views.py:150 +#: part/views.py:152 msgid "Delete Part Attachment" msgstr "Teilanhang löschen" -#: part/views.py:158 +#: part/views.py:160 msgid "Deleted part attachment" msgstr "Teilanhang gelöscht" -#: part/views.py:167 +#: part/views.py:169 #, fuzzy #| msgid "Create Part Parameter Template" msgid "Create Test Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:196 +#: part/views.py:198 #, fuzzy #| msgid "Edit Template" msgid "Edit Test Template" msgstr "Vorlage bearbeiten" -#: part/views.py:212 +#: part/views.py:214 #, fuzzy #| msgid "Delete Template" msgid "Delete Test Template" msgstr "Vorlage löschen" -#: part/views.py:221 +#: part/views.py:223 msgid "Set Part Category" msgstr "Teilkategorie auswählen" -#: part/views.py:271 +#: part/views.py:273 #, python-brace-format msgid "Set category for {n} parts" msgstr "Kategorie für {n} Teile setzen" -#: part/views.py:306 +#: part/views.py:308 msgid "Create Variant" msgstr "Variante anlegen" -#: part/views.py:386 +#: part/views.py:388 msgid "Duplicate Part" msgstr "Teil duplizieren" -#: part/views.py:393 +#: part/views.py:395 msgid "Copied part" msgstr "Teil kopiert" -#: part/views.py:518 +#: part/views.py:520 msgid "Created new part" msgstr "Neues Teil angelegt" -#: part/views.py:733 +#: part/views.py:735 msgid "Part QR Code" msgstr "Teil-QR-Code" -#: part/views.py:752 +#: part/views.py:754 msgid "Upload Part Image" msgstr "Teilbild hochladen" -#: part/views.py:760 part/views.py:797 +#: part/views.py:762 part/views.py:799 msgid "Updated part image" msgstr "Teilbild aktualisiert" -#: part/views.py:769 +#: part/views.py:771 msgid "Select Part Image" msgstr "Teilbild auswählen" -#: part/views.py:800 +#: part/views.py:802 msgid "Part image not found" msgstr "Teilbild nicht gefunden" -#: part/views.py:811 +#: part/views.py:813 msgid "Edit Part Properties" msgstr "Teileigenschaften bearbeiten" -#: part/views.py:835 +#: part/views.py:837 msgid "Validate BOM" msgstr "BOM validieren" -#: part/views.py:1002 +#: part/views.py:1004 msgid "No BOM file provided" msgstr "Keine Stückliste angegeben" -#: part/views.py:1352 +#: part/views.py:1354 msgid "Enter a valid quantity" msgstr "Bitte eine gültige Anzahl eingeben" -#: part/views.py:1377 part/views.py:1380 +#: part/views.py:1379 part/views.py:1382 msgid "Select valid part" msgstr "Bitte ein gültiges Teil auswählen" -#: part/views.py:1386 +#: part/views.py:1388 msgid "Duplicate part selected" msgstr "Teil doppelt ausgewählt" -#: part/views.py:1424 +#: part/views.py:1426 msgid "Select a part" msgstr "Teil auswählen" -#: part/views.py:1430 +#: part/views.py:1432 #, fuzzy #| msgid "Select part to be used in BOM" msgid "Selected part creates a circular BOM" msgstr "Teil für die Nutzung in der Stückliste auswählen" -#: part/views.py:1434 +#: part/views.py:1436 msgid "Specify quantity" msgstr "Anzahl angeben" -#: part/views.py:1690 +#: part/views.py:1692 msgid "Confirm Part Deletion" msgstr "Löschen des Teils bestätigen" -#: part/views.py:1699 +#: part/views.py:1701 msgid "Part was deleted" msgstr "Teil wurde gelöscht" -#: part/views.py:1708 +#: part/views.py:1710 msgid "Part Pricing" msgstr "Teilbepreisung" -#: part/views.py:1834 +#: part/views.py:1836 msgid "Create Part Parameter Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:1844 +#: part/views.py:1846 msgid "Edit Part Parameter Template" msgstr "Teilparametervorlage bearbeiten" -#: part/views.py:1853 +#: part/views.py:1855 msgid "Delete Part Parameter Template" msgstr "Teilparametervorlage löschen" -#: part/views.py:1863 +#: part/views.py:1865 msgid "Create Part Parameter" msgstr "Teilparameter anlegen" -#: part/views.py:1915 +#: part/views.py:1917 msgid "Edit Part Parameter" msgstr "Teilparameter bearbeiten" -#: part/views.py:1931 +#: part/views.py:1933 msgid "Delete Part Parameter" msgstr "Teilparameter löschen" -#: part/views.py:1990 +#: part/views.py:1992 msgid "Edit Part Category" msgstr "Teilkategorie bearbeiten" -#: part/views.py:2027 +#: part/views.py:2029 msgid "Delete Part Category" msgstr "Teilkategorie löschen" -#: part/views.py:2035 +#: part/views.py:2037 msgid "Part category was deleted" msgstr "Teilekategorie wurde gelöscht" -#: part/views.py:2098 +#: part/views.py:2100 msgid "Create BOM item" msgstr "BOM-Position anlegen" -#: part/views.py:2166 +#: part/views.py:2168 msgid "Edit BOM item" msgstr "BOM-Position beaarbeiten" -#: part/views.py:2216 +#: part/views.py:2218 msgid "Confim BOM item deletion" msgstr "Löschung von BOM-Position bestätigen" @@ -5009,76 +5031,84 @@ msgstr "Position löschen" msgid "Delete Stock" msgstr "Bestand löschen" -#: users/admin.py:62 +#: users/admin.py:61 #, fuzzy #| msgid "User" msgid "Users" msgstr "Benutzer" -#: users/admin.py:63 +#: users/admin.py:62 msgid "Select which users are assigned to this group" msgstr "" -#: users/admin.py:124 +#: users/admin.py:120 #, fuzzy #| msgid "External Link" msgid "Personal info" msgstr "Externer Link" -#: users/admin.py:125 +#: users/admin.py:121 #, fuzzy #| msgid "Revision" msgid "Permissions" msgstr "Revision" -#: users/admin.py:128 +#: users/admin.py:124 #, fuzzy #| msgid "Import BOM data" msgid "Important dates" msgstr "Stückliste importieren" -#: users/models.py:124 +#: users/models.py:128 msgid "Permission set" msgstr "" -#: users/models.py:132 +#: users/models.py:136 msgid "Group" msgstr "" -#: users/models.py:135 +#: users/models.py:139 msgid "View" msgstr "" -#: users/models.py:135 +#: users/models.py:139 msgid "Permission to view items" msgstr "" -#: users/models.py:137 -#, fuzzy -#| msgid "Created" -msgid "Create" -msgstr "Erstellt" - -#: users/models.py:137 -msgid "Permission to add items" -msgstr "" - -#: users/models.py:139 -#, fuzzy -#| msgid "Last Updated" -msgid "Update" -msgstr "Zuletzt aktualisiert" - -#: users/models.py:139 -msgid "Permissions to edit items" -msgstr "" - #: users/models.py:141 #, fuzzy +#| msgid "Address" +msgid "Add" +msgstr "Adresse" + +#: users/models.py:141 +msgid "Permission to add items" +msgstr "" + +#: users/models.py:143 +msgid "Change" +msgstr "" + +#: users/models.py:143 +msgid "Permissions to edit items" +msgstr "" + +#: users/models.py:145 +#, fuzzy #| msgid "Remove selected BOM items" msgid "Permission to delete items" msgstr "Ausgewählte Stücklistenpositionen entfernen" +#, fuzzy +#~| msgid "Created" +#~ msgid "Create" +#~ msgstr "Erstellt" + +#, fuzzy +#~| msgid "Last Updated" +#~ msgid "Update" +#~ msgstr "Zuletzt aktualisiert" + #~ msgid "Belongs To" #~ msgstr "Gehört zu" diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index 0f38022f92..74b435c791 100644 --- a/InvenTree/locale/en/LC_MESSAGES/django.po +++ b/InvenTree/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-05 13:20+0000\n" +"POT-Creation-Date: 2020-10-06 09:31+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -18,11 +18,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: InvenTree/api.py:83 +#: InvenTree/api.py:85 msgid "No action specified" msgstr "" -#: InvenTree/api.py:97 +#: InvenTree/api.py:99 msgid "No matching action found" msgstr "" @@ -46,34 +46,34 @@ msgstr "" msgid "Apply Theme" msgstr "" -#: InvenTree/helpers.py:337 order/models.py:187 order/models.py:261 +#: InvenTree/helpers.py:339 order/models.py:187 order/models.py:261 msgid "Invalid quantity provided" msgstr "" -#: InvenTree/helpers.py:340 +#: InvenTree/helpers.py:342 msgid "Empty serial number string" msgstr "" -#: InvenTree/helpers.py:361 +#: InvenTree/helpers.py:363 #, python-brace-format msgid "Duplicate serial: {n}" msgstr "" -#: InvenTree/helpers.py:365 InvenTree/helpers.py:368 InvenTree/helpers.py:371 +#: InvenTree/helpers.py:367 InvenTree/helpers.py:370 InvenTree/helpers.py:373 #, python-brace-format msgid "Invalid group: {g}" msgstr "" -#: InvenTree/helpers.py:376 +#: InvenTree/helpers.py:378 #, python-brace-format msgid "Duplicate serial: {g}" msgstr "" -#: InvenTree/helpers.py:384 +#: InvenTree/helpers.py:386 msgid "No serial numbers found" msgstr "" -#: InvenTree/helpers.py:388 +#: InvenTree/helpers.py:390 #, python-brace-format msgid "Number of unique serial number ({s}) must match quantity ({q})" msgstr "" @@ -99,19 +99,19 @@ msgstr "" msgid "Description (optional)" msgstr "" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:348 msgid "English" msgstr "" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:349 msgid "German" msgstr "" -#: InvenTree/settings.py:345 +#: InvenTree/settings.py:350 msgid "French" msgstr "" -#: InvenTree/settings.py:346 +#: InvenTree/settings.py:351 msgid "Polish" msgstr "" @@ -144,7 +144,7 @@ msgid "Returned" msgstr "" #: InvenTree/status_codes.py:136 -#: order/templates/order/sales_order_base.html:103 +#: order/templates/order/sales_order_base.html:105 msgid "Shipped" msgstr "" @@ -199,7 +199,7 @@ msgstr "" msgid "Overage must be an integer value or a percentage" msgstr "" -#: InvenTree/views.py:661 +#: InvenTree/views.py:703 msgid "Database Statistics" msgstr "" @@ -263,7 +263,7 @@ msgstr "" msgid "Build quantity must be integer value for trackable parts" msgstr "" -#: build/models.py:73 build/templates/build/build_base.html:70 +#: build/models.py:73 build/templates/build/build_base.html:72 msgid "Build Title" msgstr "" @@ -271,7 +271,7 @@ msgstr "" msgid "Brief description of the build" msgstr "" -#: build/models.py:84 build/templates/build/build_base.html:91 +#: build/models.py:84 build/templates/build/build_base.html:93 msgid "Parent Build" msgstr "" @@ -281,7 +281,7 @@ msgstr "" #: build/models.py:90 build/templates/build/allocate.html:329 #: build/templates/build/auto_allocate.html:19 -#: build/templates/build/build_base.html:75 +#: build/templates/build/build_base.html:77 #: build/templates/build/detail.html:22 order/models.py:501 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:147 @@ -403,7 +403,7 @@ msgid "Stock quantity to allocate to build" msgstr "" #: build/templates/build/allocate.html:17 -#: company/templates/company/detail_part.html:18 order/views.py:779 +#: company/templates/company/detail_part.html:18 order/views.py:804 #: part/templates/part/category.html:122 msgid "Order Parts" msgstr "" @@ -437,7 +437,7 @@ msgstr "" #: build/templates/build/allocate.html:172 #: build/templates/build/auto_allocate.html:20 -#: build/templates/build/build_base.html:80 +#: build/templates/build/build_base.html:82 #: build/templates/build/detail.html:27 #: company/templates/company/supplier_part_pricing.html:71 #: order/templates/order/order_wizard/select_parts.html:32 @@ -570,11 +570,27 @@ msgstr "" msgid "Admin view" msgstr "" -#: build/templates/build/build_base.html:66 build/templates/build/detail.html:9 +#: build/templates/build/build_base.html:45 +msgid "Edit Build" +msgstr "" + +#: build/templates/build/build_base.html:49 build/views.py:190 +msgid "Complete Build" +msgstr "" + +#: build/templates/build/build_base.html:52 build/views.py:58 +msgid "Cancel Build" +msgstr "" + +#: build/templates/build/build_base.html:58 build/views.py:454 +msgid "Delete Build" +msgstr "" + +#: build/templates/build/build_base.html:68 build/templates/build/detail.html:9 msgid "Build Details" msgstr "" -#: build/templates/build/build_base.html:85 +#: build/templates/build/build_base.html:87 #: build/templates/build/detail.html:42 #: order/templates/order/receive_parts.html:24 #: stock/templates/stock/item_base.html:276 templates/InvenTree/search.html:175 @@ -584,7 +600,7 @@ msgstr "" msgid "Status" msgstr "" -#: build/templates/build/build_base.html:98 order/models.py:499 +#: build/templates/build/build_base.html:100 order/models.py:499 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 @@ -594,15 +610,15 @@ msgstr "" msgid "Sales Order" msgstr "" -#: build/templates/build/build_base.html:104 +#: build/templates/build/build_base.html:106 msgid "BOM Price" msgstr "" -#: build/templates/build/build_base.html:109 +#: build/templates/build/build_base.html:111 msgid "BOM pricing is incomplete" msgstr "" -#: build/templates/build/build_base.html:112 +#: build/templates/build/build_base.html:114 msgid "No pricing information" msgstr "" @@ -664,8 +680,8 @@ msgid "Batch" msgstr "" #: build/templates/build/detail.html:61 -#: order/templates/order/order_base.html:98 -#: order/templates/order/sales_order_base.html:97 templates/js/build.html:71 +#: order/templates/order/order_base.html:100 +#: order/templates/order/sales_order_base.html:99 templates/js/build.html:71 msgid "Created" msgstr "" @@ -707,7 +723,7 @@ msgid "Save" msgstr "" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 -#: order/templates/order/order_notes.html:32 +#: order/templates/order/order_notes.html:33 #: order/templates/order/sales_order_notes.html:37 #: part/templates/part/notes.html:33 stock/templates/stock/item_notes.html:33 msgid "Edit notes" @@ -726,100 +742,88 @@ msgstr "" msgid "Are you sure you wish to unallocate all stock for this build?" msgstr "" -#: build/views.py:56 -msgid "Cancel Build" -msgstr "" - -#: build/views.py:74 +#: build/views.py:77 msgid "Confirm build cancellation" msgstr "" -#: build/views.py:79 +#: build/views.py:82 msgid "Build was cancelled" msgstr "" -#: build/views.py:95 +#: build/views.py:98 msgid "Allocate Stock" msgstr "" -#: build/views.py:108 +#: build/views.py:112 msgid "No matching build found" msgstr "" -#: build/views.py:127 +#: build/views.py:131 msgid "Confirm stock allocation" msgstr "" -#: build/views.py:128 +#: build/views.py:132 msgid "Check the confirmation box at the bottom of the list" msgstr "" -#: build/views.py:148 build/views.py:452 +#: build/views.py:152 build/views.py:465 msgid "Unallocate Stock" msgstr "" -#: build/views.py:161 +#: build/views.py:166 msgid "Confirm unallocation of build stock" msgstr "" -#: build/views.py:162 stock/views.py:405 +#: build/views.py:167 stock/views.py:405 msgid "Check the confirmation box" msgstr "" -#: build/views.py:185 -msgid "Complete Build" -msgstr "" - -#: build/views.py:264 +#: build/views.py:270 msgid "Confirm completion of build" msgstr "" -#: build/views.py:271 +#: build/views.py:277 msgid "Invalid location selected" msgstr "" -#: build/views.py:296 stock/views.py:1621 +#: build/views.py:302 stock/views.py:1621 #, python-brace-format msgid "The following serial numbers already exist: ({sn})" msgstr "" -#: build/views.py:317 +#: build/views.py:323 msgid "Build marked as COMPLETE" msgstr "" -#: build/views.py:393 +#: build/views.py:403 msgid "Start new Build" msgstr "" -#: build/views.py:418 +#: build/views.py:429 msgid "Created new build" msgstr "" -#: build/views.py:428 +#: build/views.py:439 msgid "Edit Build Details" msgstr "" -#: build/views.py:433 +#: build/views.py:445 msgid "Edited build" msgstr "" -#: build/views.py:442 -msgid "Delete Build" -msgstr "" - -#: build/views.py:457 +#: build/views.py:471 msgid "Removed parts from build allocation" msgstr "" -#: build/views.py:467 +#: build/views.py:481 msgid "Allocate new Part" msgstr "" -#: build/views.py:620 +#: build/views.py:635 msgid "Edit Stock Allocation" msgstr "" -#: build/views.py:624 +#: build/views.py:640 msgid "Updated Build Item" msgstr "" @@ -1014,7 +1018,7 @@ msgstr "" #: company/templates/company/detail.html:21 #: company/templates/company/supplier_part_base.html:66 #: company/templates/company/supplier_part_detail.html:21 -#: order/templates/order/order_base.html:79 +#: order/templates/order/order_base.html:81 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 #: stock/templates/stock/item_base.html:251 templates/js/company.html:48 #: templates/js/company.html:162 templates/js/order.html:146 @@ -1022,7 +1026,7 @@ msgid "Supplier" msgstr "" #: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:78 stock/models.py:370 +#: order/templates/order/sales_order_base.html:80 stock/models.py:370 #: stock/models.py:371 stock/templates/stock/item_base.html:169 #: templates/js/company.html:40 templates/js/order.html:221 msgid "Customer" @@ -1123,12 +1127,12 @@ msgid "Purchase Orders" msgstr "" #: company/templates/company/purchase_orders.html:14 -#: order/templates/order/purchase_orders.html:17 +#: order/templates/order/purchase_orders.html:18 msgid "Create new purchase order" msgstr "" #: company/templates/company/purchase_orders.html:14 -#: order/templates/order/purchase_orders.html:17 +#: order/templates/order/purchase_orders.html:18 msgid "New Purchase Order" msgstr "" @@ -1142,12 +1146,12 @@ msgid "Sales Orders" msgstr "" #: company/templates/company/sales_orders.html:14 -#: order/templates/order/sales_orders.html:17 +#: order/templates/order/sales_orders.html:18 msgid "Create new sales order" msgstr "" #: company/templates/company/sales_orders.html:14 -#: order/templates/order/sales_orders.html:17 +#: order/templates/order/sales_orders.html:18 msgid "New Sales Order" msgstr "" @@ -1205,7 +1209,7 @@ msgid "Pricing Information" msgstr "" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2226 +#: part/templates/part/sale_prices.html:13 part/views.py:2228 msgid "Add Price Break" msgstr "" @@ -1330,15 +1334,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:404 part/views.py:2232 +#: company/views.py:404 part/views.py:2234 msgid "Added new price break" msgstr "" -#: company/views.py:441 part/views.py:2277 +#: company/views.py:441 part/views.py:2279 msgid "Edit Price Break" msgstr "" -#: company/views.py:456 part/views.py:2293 +#: company/views.py:456 part/views.py:2295 msgid "Delete Price Break" msgstr "" @@ -1366,20 +1370,20 @@ msgstr "" msgid "Enabled" msgstr "" -#: order/forms.py:24 +#: order/forms.py:24 order/templates/order/order_base.html:40 msgid "Place order" msgstr "" -#: order/forms.py:35 +#: order/forms.py:35 order/templates/order/order_base.html:47 msgid "Mark order as complete" msgstr "" -#: order/forms.py:46 order/forms.py:57 -#: order/templates/order/sales_order_base.html:54 +#: order/forms.py:46 order/forms.py:57 order/templates/order/order_base.html:52 +#: order/templates/order/sales_order_base.html:52 msgid "Cancel order" msgstr "" -#: order/forms.py:68 order/templates/order/sales_order_base.html:51 +#: order/forms.py:68 order/templates/order/sales_order_base.html:49 msgid "Ship order" msgstr "" @@ -1431,7 +1435,7 @@ msgstr "" msgid "Date order was completed" msgstr "" -#: order/models.py:185 order/models.py:259 part/views.py:1343 +#: order/models.py:185 order/models.py:259 part/views.py:1345 #: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "" @@ -1512,32 +1516,44 @@ msgstr "" msgid "Are you sure you want to delete this attachment?" msgstr "" -#: order/templates/order/order_base.html:64 +#: order/templates/order/order_base.html:36 +msgid "Edit order information" +msgstr "" + +#: order/templates/order/order_base.html:44 +msgid "Receive items" +msgstr "" + +#: order/templates/order/order_base.html:57 +msgid "Export order to file" +msgstr "" + +#: order/templates/order/order_base.html:66 msgid "Purchase Order Details" msgstr "" -#: order/templates/order/order_base.html:69 -#: order/templates/order/sales_order_base.html:68 +#: order/templates/order/order_base.html:71 +#: order/templates/order/sales_order_base.html:70 msgid "Order Reference" msgstr "" -#: order/templates/order/order_base.html:74 -#: order/templates/order/sales_order_base.html:73 +#: order/templates/order/order_base.html:76 +#: order/templates/order/sales_order_base.html:75 msgid "Order Status" msgstr "" -#: order/templates/order/order_base.html:85 templates/js/order.html:153 +#: order/templates/order/order_base.html:87 templates/js/order.html:153 msgid "Supplier Reference" msgstr "" -#: order/templates/order/order_base.html:104 +#: order/templates/order/order_base.html:106 msgid "Issued" msgstr "" -#: order/templates/order/order_base.html:111 +#: order/templates/order/order_base.html:113 #: order/templates/order/purchase_order_detail.html:182 #: order/templates/order/receive_parts.html:22 -#: order/templates/order/sales_order_base.html:110 +#: order/templates/order/sales_order_base.html:112 msgid "Received" msgstr "" @@ -1605,8 +1621,8 @@ msgid "Attachments" msgstr "" #: order/templates/order/purchase_order_detail.html:16 -#: order/templates/order/sales_order_detail.html:17 order/views.py:1087 -#: order/views.py:1201 +#: order/templates/order/sales_order_detail.html:17 order/views.py:1117 +#: order/views.py:1232 msgid "Add Line Item" msgstr "" @@ -1674,15 +1690,15 @@ msgstr "" msgid "This SalesOrder has not been fully allocated" msgstr "" -#: order/templates/order/sales_order_base.html:47 +#: order/templates/order/sales_order_base.html:57 msgid "Packing List" msgstr "" -#: order/templates/order/sales_order_base.html:63 +#: order/templates/order/sales_order_base.html:65 msgid "Sales Order Details" msgstr "" -#: order/templates/order/sales_order_base.html:84 templates/js/order.html:228 +#: order/templates/order/sales_order_base.html:86 templates/js/order.html:228 msgid "Customer Reference" msgstr "" @@ -1746,147 +1762,147 @@ msgstr "" msgid "Order Items" msgstr "" -#: order/views.py:93 +#: order/views.py:99 msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:102 order/views.py:149 part/views.py:90 stock/views.py:167 +#: order/views.py:109 order/views.py:157 part/views.py:92 stock/views.py:167 msgid "Added attachment" msgstr "" -#: order/views.py:141 +#: order/views.py:148 msgid "Add Sales Order Attachment" msgstr "" -#: order/views.py:176 order/views.py:197 +#: order/views.py:184 order/views.py:206 msgid "Edit Attachment" msgstr "" -#: order/views.py:180 order/views.py:201 +#: order/views.py:189 order/views.py:211 msgid "Attachment updated" msgstr "" -#: order/views.py:216 order/views.py:230 +#: order/views.py:226 order/views.py:241 msgid "Delete Attachment" msgstr "" -#: order/views.py:222 order/views.py:236 stock/views.py:223 +#: order/views.py:233 order/views.py:248 stock/views.py:223 msgid "Deleted attachment" msgstr "" -#: order/views.py:287 +#: order/views.py:301 msgid "Create Purchase Order" msgstr "" -#: order/views.py:318 +#: order/views.py:333 msgid "Create Sales Order" msgstr "" -#: order/views.py:348 +#: order/views.py:364 msgid "Edit Purchase Order" msgstr "" -#: order/views.py:368 +#: order/views.py:385 msgid "Edit Sales Order" msgstr "" -#: order/views.py:384 +#: order/views.py:402 msgid "Cancel Order" msgstr "" -#: order/views.py:399 order/views.py:431 +#: order/views.py:418 order/views.py:451 msgid "Confirm order cancellation" msgstr "" -#: order/views.py:417 +#: order/views.py:436 msgid "Cancel sales order" msgstr "" -#: order/views.py:437 +#: order/views.py:457 msgid "Could not cancel order" msgstr "" -#: order/views.py:451 +#: order/views.py:471 msgid "Issue Order" msgstr "" -#: order/views.py:466 +#: order/views.py:487 msgid "Confirm order placement" msgstr "" -#: order/views.py:487 +#: order/views.py:508 msgid "Complete Order" msgstr "" -#: order/views.py:522 +#: order/views.py:544 msgid "Ship Order" msgstr "" -#: order/views.py:538 +#: order/views.py:561 msgid "Confirm order shipment" msgstr "" -#: order/views.py:544 +#: order/views.py:567 msgid "Could not ship order" msgstr "" -#: order/views.py:595 +#: order/views.py:619 msgid "Receive Parts" msgstr "" -#: order/views.py:662 +#: order/views.py:687 msgid "Items received" msgstr "" -#: order/views.py:676 +#: order/views.py:701 msgid "No destination set" msgstr "" -#: order/views.py:721 +#: order/views.py:746 msgid "Error converting quantity to number" msgstr "" -#: order/views.py:727 +#: order/views.py:752 msgid "Receive quantity less than zero" msgstr "" -#: order/views.py:733 +#: order/views.py:758 msgid "No lines specified" msgstr "" -#: order/views.py:1107 +#: order/views.py:1138 msgid "Invalid Purchase Order" msgstr "" -#: order/views.py:1115 +#: order/views.py:1146 msgid "Supplier must match for Part and Order" msgstr "" -#: order/views.py:1120 +#: order/views.py:1151 msgid "Invalid SupplierPart selection" msgstr "" -#: order/views.py:1252 order/views.py:1270 +#: order/views.py:1284 order/views.py:1303 msgid "Edit Line Item" msgstr "" -#: order/views.py:1286 order/views.py:1298 +#: order/views.py:1320 order/views.py:1333 msgid "Delete Line Item" msgstr "" -#: order/views.py:1291 order/views.py:1303 +#: order/views.py:1326 order/views.py:1339 msgid "Deleted line item" msgstr "" -#: order/views.py:1312 +#: order/views.py:1348 msgid "Allocate Stock to Order" msgstr "" -#: order/views.py:1381 +#: order/views.py:1418 msgid "Edit Allocation Quantity" msgstr "" -#: order/views.py:1396 +#: order/views.py:1434 msgid "Remove allocation" msgstr "" @@ -2226,7 +2242,7 @@ msgstr "" msgid "BOM line checksum" msgstr "" -#: part/models.py:1612 part/views.py:1349 part/views.py:1401 +#: part/models.py:1612 part/views.py:1351 part/views.py:1403 #: stock/models.py:231 msgid "Quantity must be integer value for trackable parts" msgstr "" @@ -2293,7 +2309,7 @@ msgstr "" msgid "Validate Bill of Materials" msgstr "" -#: part/templates/part/bom.html:48 part/views.py:1640 +#: part/templates/part/bom.html:48 part/views.py:1642 msgid "Export Bill of Materials" msgstr "" @@ -2385,7 +2401,7 @@ msgstr "" msgid "All parts" msgstr "" -#: part/templates/part/category.html:24 part/views.py:2043 +#: part/templates/part/category.html:24 part/views.py:2045 msgid "Create new part category" msgstr "" @@ -2425,7 +2441,7 @@ msgstr "" msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:114 part/views.py:511 +#: part/templates/part/category.html:114 part/views.py:513 msgid "Create new part" msgstr "" @@ -2642,7 +2658,7 @@ msgid "Edit" msgstr "" #: part/templates/part/params.html:39 part/templates/part/supplier.html:17 -#: users/models.py:141 +#: users/models.py:145 msgid "Delete" msgstr "" @@ -2842,176 +2858,176 @@ msgstr "" msgid "New Variant" msgstr "" -#: part/views.py:78 +#: part/views.py:80 msgid "Add part attachment" msgstr "" -#: part/views.py:129 templates/attachment_table.html:30 +#: part/views.py:131 templates/attachment_table.html:30 msgid "Edit attachment" msgstr "" -#: part/views.py:135 +#: part/views.py:137 msgid "Part attachment updated" msgstr "" -#: part/views.py:150 +#: part/views.py:152 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:158 +#: part/views.py:160 msgid "Deleted part attachment" msgstr "" -#: part/views.py:167 +#: part/views.py:169 msgid "Create Test Template" msgstr "" -#: part/views.py:196 +#: part/views.py:198 msgid "Edit Test Template" msgstr "" -#: part/views.py:212 +#: part/views.py:214 msgid "Delete Test Template" msgstr "" -#: part/views.py:221 +#: part/views.py:223 msgid "Set Part Category" msgstr "" -#: part/views.py:271 +#: part/views.py:273 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:306 +#: part/views.py:308 msgid "Create Variant" msgstr "" -#: part/views.py:386 +#: part/views.py:388 msgid "Duplicate Part" msgstr "" -#: part/views.py:393 +#: part/views.py:395 msgid "Copied part" msgstr "" -#: part/views.py:518 +#: part/views.py:520 msgid "Created new part" msgstr "" -#: part/views.py:733 +#: part/views.py:735 msgid "Part QR Code" msgstr "" -#: part/views.py:752 +#: part/views.py:754 msgid "Upload Part Image" msgstr "" -#: part/views.py:760 part/views.py:797 +#: part/views.py:762 part/views.py:799 msgid "Updated part image" msgstr "" -#: part/views.py:769 +#: part/views.py:771 msgid "Select Part Image" msgstr "" -#: part/views.py:800 +#: part/views.py:802 msgid "Part image not found" msgstr "" -#: part/views.py:811 +#: part/views.py:813 msgid "Edit Part Properties" msgstr "" -#: part/views.py:835 +#: part/views.py:837 msgid "Validate BOM" msgstr "" -#: part/views.py:1002 +#: part/views.py:1004 msgid "No BOM file provided" msgstr "" -#: part/views.py:1352 +#: part/views.py:1354 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1377 part/views.py:1380 +#: part/views.py:1379 part/views.py:1382 msgid "Select valid part" msgstr "" -#: part/views.py:1386 +#: part/views.py:1388 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1424 +#: part/views.py:1426 msgid "Select a part" msgstr "" -#: part/views.py:1430 +#: part/views.py:1432 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1434 +#: part/views.py:1436 msgid "Specify quantity" msgstr "" -#: part/views.py:1690 +#: part/views.py:1692 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1699 +#: part/views.py:1701 msgid "Part was deleted" msgstr "" -#: part/views.py:1708 +#: part/views.py:1710 msgid "Part Pricing" msgstr "" -#: part/views.py:1834 +#: part/views.py:1836 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1844 +#: part/views.py:1846 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1853 +#: part/views.py:1855 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1863 +#: part/views.py:1865 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1915 +#: part/views.py:1917 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:1931 +#: part/views.py:1933 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1990 +#: part/views.py:1992 msgid "Edit Part Category" msgstr "" -#: part/views.py:2027 +#: part/views.py:2029 msgid "Delete Part Category" msgstr "" -#: part/views.py:2035 +#: part/views.py:2037 msgid "Part category was deleted" msgstr "" -#: part/views.py:2098 +#: part/views.py:2100 msgid "Create BOM item" msgstr "" -#: part/views.py:2166 +#: part/views.py:2168 msgid "Edit BOM item" msgstr "" -#: part/views.py:2216 +#: part/views.py:2218 msgid "Confim BOM item deletion" msgstr "" @@ -4497,58 +4513,58 @@ msgstr "" msgid "Delete Stock" msgstr "" -#: users/admin.py:62 +#: users/admin.py:61 msgid "Users" msgstr "" -#: users/admin.py:63 +#: users/admin.py:62 msgid "Select which users are assigned to this group" msgstr "" -#: users/admin.py:124 +#: users/admin.py:120 msgid "Personal info" msgstr "" -#: users/admin.py:125 +#: users/admin.py:121 msgid "Permissions" msgstr "" -#: users/admin.py:128 +#: users/admin.py:124 msgid "Important dates" msgstr "" -#: users/models.py:124 +#: users/models.py:128 msgid "Permission set" msgstr "" -#: users/models.py:132 +#: users/models.py:136 msgid "Group" msgstr "" -#: users/models.py:135 +#: users/models.py:139 msgid "View" msgstr "" -#: users/models.py:135 +#: users/models.py:139 msgid "Permission to view items" msgstr "" -#: users/models.py:137 -msgid "Create" -msgstr "" - -#: users/models.py:137 -msgid "Permission to add items" -msgstr "" - -#: users/models.py:139 -msgid "Update" -msgstr "" - -#: users/models.py:139 -msgid "Permissions to edit items" +#: users/models.py:141 +msgid "Add" msgstr "" #: users/models.py:141 +msgid "Permission to add items" +msgstr "" + +#: users/models.py:143 +msgid "Change" +msgstr "" + +#: users/models.py:143 +msgid "Permissions to edit items" +msgstr "" + +#: users/models.py:145 msgid "Permission to delete items" msgstr "" diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po index 0f38022f92..74b435c791 100644 --- a/InvenTree/locale/es/LC_MESSAGES/django.po +++ b/InvenTree/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-05 13:20+0000\n" +"POT-Creation-Date: 2020-10-06 09:31+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -18,11 +18,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: InvenTree/api.py:83 +#: InvenTree/api.py:85 msgid "No action specified" msgstr "" -#: InvenTree/api.py:97 +#: InvenTree/api.py:99 msgid "No matching action found" msgstr "" @@ -46,34 +46,34 @@ msgstr "" msgid "Apply Theme" msgstr "" -#: InvenTree/helpers.py:337 order/models.py:187 order/models.py:261 +#: InvenTree/helpers.py:339 order/models.py:187 order/models.py:261 msgid "Invalid quantity provided" msgstr "" -#: InvenTree/helpers.py:340 +#: InvenTree/helpers.py:342 msgid "Empty serial number string" msgstr "" -#: InvenTree/helpers.py:361 +#: InvenTree/helpers.py:363 #, python-brace-format msgid "Duplicate serial: {n}" msgstr "" -#: InvenTree/helpers.py:365 InvenTree/helpers.py:368 InvenTree/helpers.py:371 +#: InvenTree/helpers.py:367 InvenTree/helpers.py:370 InvenTree/helpers.py:373 #, python-brace-format msgid "Invalid group: {g}" msgstr "" -#: InvenTree/helpers.py:376 +#: InvenTree/helpers.py:378 #, python-brace-format msgid "Duplicate serial: {g}" msgstr "" -#: InvenTree/helpers.py:384 +#: InvenTree/helpers.py:386 msgid "No serial numbers found" msgstr "" -#: InvenTree/helpers.py:388 +#: InvenTree/helpers.py:390 #, python-brace-format msgid "Number of unique serial number ({s}) must match quantity ({q})" msgstr "" @@ -99,19 +99,19 @@ msgstr "" msgid "Description (optional)" msgstr "" -#: InvenTree/settings.py:343 +#: InvenTree/settings.py:348 msgid "English" msgstr "" -#: InvenTree/settings.py:344 +#: InvenTree/settings.py:349 msgid "German" msgstr "" -#: InvenTree/settings.py:345 +#: InvenTree/settings.py:350 msgid "French" msgstr "" -#: InvenTree/settings.py:346 +#: InvenTree/settings.py:351 msgid "Polish" msgstr "" @@ -144,7 +144,7 @@ msgid "Returned" msgstr "" #: InvenTree/status_codes.py:136 -#: order/templates/order/sales_order_base.html:103 +#: order/templates/order/sales_order_base.html:105 msgid "Shipped" msgstr "" @@ -199,7 +199,7 @@ msgstr "" msgid "Overage must be an integer value or a percentage" msgstr "" -#: InvenTree/views.py:661 +#: InvenTree/views.py:703 msgid "Database Statistics" msgstr "" @@ -263,7 +263,7 @@ msgstr "" msgid "Build quantity must be integer value for trackable parts" msgstr "" -#: build/models.py:73 build/templates/build/build_base.html:70 +#: build/models.py:73 build/templates/build/build_base.html:72 msgid "Build Title" msgstr "" @@ -271,7 +271,7 @@ msgstr "" msgid "Brief description of the build" msgstr "" -#: build/models.py:84 build/templates/build/build_base.html:91 +#: build/models.py:84 build/templates/build/build_base.html:93 msgid "Parent Build" msgstr "" @@ -281,7 +281,7 @@ msgstr "" #: build/models.py:90 build/templates/build/allocate.html:329 #: build/templates/build/auto_allocate.html:19 -#: build/templates/build/build_base.html:75 +#: build/templates/build/build_base.html:77 #: build/templates/build/detail.html:22 order/models.py:501 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:147 @@ -403,7 +403,7 @@ msgid "Stock quantity to allocate to build" msgstr "" #: build/templates/build/allocate.html:17 -#: company/templates/company/detail_part.html:18 order/views.py:779 +#: company/templates/company/detail_part.html:18 order/views.py:804 #: part/templates/part/category.html:122 msgid "Order Parts" msgstr "" @@ -437,7 +437,7 @@ msgstr "" #: build/templates/build/allocate.html:172 #: build/templates/build/auto_allocate.html:20 -#: build/templates/build/build_base.html:80 +#: build/templates/build/build_base.html:82 #: build/templates/build/detail.html:27 #: company/templates/company/supplier_part_pricing.html:71 #: order/templates/order/order_wizard/select_parts.html:32 @@ -570,11 +570,27 @@ msgstr "" msgid "Admin view" msgstr "" -#: build/templates/build/build_base.html:66 build/templates/build/detail.html:9 +#: build/templates/build/build_base.html:45 +msgid "Edit Build" +msgstr "" + +#: build/templates/build/build_base.html:49 build/views.py:190 +msgid "Complete Build" +msgstr "" + +#: build/templates/build/build_base.html:52 build/views.py:58 +msgid "Cancel Build" +msgstr "" + +#: build/templates/build/build_base.html:58 build/views.py:454 +msgid "Delete Build" +msgstr "" + +#: build/templates/build/build_base.html:68 build/templates/build/detail.html:9 msgid "Build Details" msgstr "" -#: build/templates/build/build_base.html:85 +#: build/templates/build/build_base.html:87 #: build/templates/build/detail.html:42 #: order/templates/order/receive_parts.html:24 #: stock/templates/stock/item_base.html:276 templates/InvenTree/search.html:175 @@ -584,7 +600,7 @@ msgstr "" msgid "Status" msgstr "" -#: build/templates/build/build_base.html:98 order/models.py:499 +#: build/templates/build/build_base.html:100 order/models.py:499 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 @@ -594,15 +610,15 @@ msgstr "" msgid "Sales Order" msgstr "" -#: build/templates/build/build_base.html:104 +#: build/templates/build/build_base.html:106 msgid "BOM Price" msgstr "" -#: build/templates/build/build_base.html:109 +#: build/templates/build/build_base.html:111 msgid "BOM pricing is incomplete" msgstr "" -#: build/templates/build/build_base.html:112 +#: build/templates/build/build_base.html:114 msgid "No pricing information" msgstr "" @@ -664,8 +680,8 @@ msgid "Batch" msgstr "" #: build/templates/build/detail.html:61 -#: order/templates/order/order_base.html:98 -#: order/templates/order/sales_order_base.html:97 templates/js/build.html:71 +#: order/templates/order/order_base.html:100 +#: order/templates/order/sales_order_base.html:99 templates/js/build.html:71 msgid "Created" msgstr "" @@ -707,7 +723,7 @@ msgid "Save" msgstr "" #: build/templates/build/notes.html:33 company/templates/company/notes.html:30 -#: order/templates/order/order_notes.html:32 +#: order/templates/order/order_notes.html:33 #: order/templates/order/sales_order_notes.html:37 #: part/templates/part/notes.html:33 stock/templates/stock/item_notes.html:33 msgid "Edit notes" @@ -726,100 +742,88 @@ msgstr "" msgid "Are you sure you wish to unallocate all stock for this build?" msgstr "" -#: build/views.py:56 -msgid "Cancel Build" -msgstr "" - -#: build/views.py:74 +#: build/views.py:77 msgid "Confirm build cancellation" msgstr "" -#: build/views.py:79 +#: build/views.py:82 msgid "Build was cancelled" msgstr "" -#: build/views.py:95 +#: build/views.py:98 msgid "Allocate Stock" msgstr "" -#: build/views.py:108 +#: build/views.py:112 msgid "No matching build found" msgstr "" -#: build/views.py:127 +#: build/views.py:131 msgid "Confirm stock allocation" msgstr "" -#: build/views.py:128 +#: build/views.py:132 msgid "Check the confirmation box at the bottom of the list" msgstr "" -#: build/views.py:148 build/views.py:452 +#: build/views.py:152 build/views.py:465 msgid "Unallocate Stock" msgstr "" -#: build/views.py:161 +#: build/views.py:166 msgid "Confirm unallocation of build stock" msgstr "" -#: build/views.py:162 stock/views.py:405 +#: build/views.py:167 stock/views.py:405 msgid "Check the confirmation box" msgstr "" -#: build/views.py:185 -msgid "Complete Build" -msgstr "" - -#: build/views.py:264 +#: build/views.py:270 msgid "Confirm completion of build" msgstr "" -#: build/views.py:271 +#: build/views.py:277 msgid "Invalid location selected" msgstr "" -#: build/views.py:296 stock/views.py:1621 +#: build/views.py:302 stock/views.py:1621 #, python-brace-format msgid "The following serial numbers already exist: ({sn})" msgstr "" -#: build/views.py:317 +#: build/views.py:323 msgid "Build marked as COMPLETE" msgstr "" -#: build/views.py:393 +#: build/views.py:403 msgid "Start new Build" msgstr "" -#: build/views.py:418 +#: build/views.py:429 msgid "Created new build" msgstr "" -#: build/views.py:428 +#: build/views.py:439 msgid "Edit Build Details" msgstr "" -#: build/views.py:433 +#: build/views.py:445 msgid "Edited build" msgstr "" -#: build/views.py:442 -msgid "Delete Build" -msgstr "" - -#: build/views.py:457 +#: build/views.py:471 msgid "Removed parts from build allocation" msgstr "" -#: build/views.py:467 +#: build/views.py:481 msgid "Allocate new Part" msgstr "" -#: build/views.py:620 +#: build/views.py:635 msgid "Edit Stock Allocation" msgstr "" -#: build/views.py:624 +#: build/views.py:640 msgid "Updated Build Item" msgstr "" @@ -1014,7 +1018,7 @@ msgstr "" #: company/templates/company/detail.html:21 #: company/templates/company/supplier_part_base.html:66 #: company/templates/company/supplier_part_detail.html:21 -#: order/templates/order/order_base.html:79 +#: order/templates/order/order_base.html:81 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 #: stock/templates/stock/item_base.html:251 templates/js/company.html:48 #: templates/js/company.html:162 templates/js/order.html:146 @@ -1022,7 +1026,7 @@ msgid "Supplier" msgstr "" #: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:78 stock/models.py:370 +#: order/templates/order/sales_order_base.html:80 stock/models.py:370 #: stock/models.py:371 stock/templates/stock/item_base.html:169 #: templates/js/company.html:40 templates/js/order.html:221 msgid "Customer" @@ -1123,12 +1127,12 @@ msgid "Purchase Orders" msgstr "" #: company/templates/company/purchase_orders.html:14 -#: order/templates/order/purchase_orders.html:17 +#: order/templates/order/purchase_orders.html:18 msgid "Create new purchase order" msgstr "" #: company/templates/company/purchase_orders.html:14 -#: order/templates/order/purchase_orders.html:17 +#: order/templates/order/purchase_orders.html:18 msgid "New Purchase Order" msgstr "" @@ -1142,12 +1146,12 @@ msgid "Sales Orders" msgstr "" #: company/templates/company/sales_orders.html:14 -#: order/templates/order/sales_orders.html:17 +#: order/templates/order/sales_orders.html:18 msgid "Create new sales order" msgstr "" #: company/templates/company/sales_orders.html:14 -#: order/templates/order/sales_orders.html:17 +#: order/templates/order/sales_orders.html:18 msgid "New Sales Order" msgstr "" @@ -1205,7 +1209,7 @@ msgid "Pricing Information" msgstr "" #: company/templates/company/supplier_part_pricing.html:15 company/views.py:399 -#: part/templates/part/sale_prices.html:13 part/views.py:2226 +#: part/templates/part/sale_prices.html:13 part/views.py:2228 msgid "Add Price Break" msgstr "" @@ -1330,15 +1334,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:404 part/views.py:2232 +#: company/views.py:404 part/views.py:2234 msgid "Added new price break" msgstr "" -#: company/views.py:441 part/views.py:2277 +#: company/views.py:441 part/views.py:2279 msgid "Edit Price Break" msgstr "" -#: company/views.py:456 part/views.py:2293 +#: company/views.py:456 part/views.py:2295 msgid "Delete Price Break" msgstr "" @@ -1366,20 +1370,20 @@ msgstr "" msgid "Enabled" msgstr "" -#: order/forms.py:24 +#: order/forms.py:24 order/templates/order/order_base.html:40 msgid "Place order" msgstr "" -#: order/forms.py:35 +#: order/forms.py:35 order/templates/order/order_base.html:47 msgid "Mark order as complete" msgstr "" -#: order/forms.py:46 order/forms.py:57 -#: order/templates/order/sales_order_base.html:54 +#: order/forms.py:46 order/forms.py:57 order/templates/order/order_base.html:52 +#: order/templates/order/sales_order_base.html:52 msgid "Cancel order" msgstr "" -#: order/forms.py:68 order/templates/order/sales_order_base.html:51 +#: order/forms.py:68 order/templates/order/sales_order_base.html:49 msgid "Ship order" msgstr "" @@ -1431,7 +1435,7 @@ msgstr "" msgid "Date order was completed" msgstr "" -#: order/models.py:185 order/models.py:259 part/views.py:1343 +#: order/models.py:185 order/models.py:259 part/views.py:1345 #: stock/models.py:241 stock/models.py:805 msgid "Quantity must be greater than zero" msgstr "" @@ -1512,32 +1516,44 @@ msgstr "" msgid "Are you sure you want to delete this attachment?" msgstr "" -#: order/templates/order/order_base.html:64 +#: order/templates/order/order_base.html:36 +msgid "Edit order information" +msgstr "" + +#: order/templates/order/order_base.html:44 +msgid "Receive items" +msgstr "" + +#: order/templates/order/order_base.html:57 +msgid "Export order to file" +msgstr "" + +#: order/templates/order/order_base.html:66 msgid "Purchase Order Details" msgstr "" -#: order/templates/order/order_base.html:69 -#: order/templates/order/sales_order_base.html:68 +#: order/templates/order/order_base.html:71 +#: order/templates/order/sales_order_base.html:70 msgid "Order Reference" msgstr "" -#: order/templates/order/order_base.html:74 -#: order/templates/order/sales_order_base.html:73 +#: order/templates/order/order_base.html:76 +#: order/templates/order/sales_order_base.html:75 msgid "Order Status" msgstr "" -#: order/templates/order/order_base.html:85 templates/js/order.html:153 +#: order/templates/order/order_base.html:87 templates/js/order.html:153 msgid "Supplier Reference" msgstr "" -#: order/templates/order/order_base.html:104 +#: order/templates/order/order_base.html:106 msgid "Issued" msgstr "" -#: order/templates/order/order_base.html:111 +#: order/templates/order/order_base.html:113 #: order/templates/order/purchase_order_detail.html:182 #: order/templates/order/receive_parts.html:22 -#: order/templates/order/sales_order_base.html:110 +#: order/templates/order/sales_order_base.html:112 msgid "Received" msgstr "" @@ -1605,8 +1621,8 @@ msgid "Attachments" msgstr "" #: order/templates/order/purchase_order_detail.html:16 -#: order/templates/order/sales_order_detail.html:17 order/views.py:1087 -#: order/views.py:1201 +#: order/templates/order/sales_order_detail.html:17 order/views.py:1117 +#: order/views.py:1232 msgid "Add Line Item" msgstr "" @@ -1674,15 +1690,15 @@ msgstr "" msgid "This SalesOrder has not been fully allocated" msgstr "" -#: order/templates/order/sales_order_base.html:47 +#: order/templates/order/sales_order_base.html:57 msgid "Packing List" msgstr "" -#: order/templates/order/sales_order_base.html:63 +#: order/templates/order/sales_order_base.html:65 msgid "Sales Order Details" msgstr "" -#: order/templates/order/sales_order_base.html:84 templates/js/order.html:228 +#: order/templates/order/sales_order_base.html:86 templates/js/order.html:228 msgid "Customer Reference" msgstr "" @@ -1746,147 +1762,147 @@ msgstr "" msgid "Order Items" msgstr "" -#: order/views.py:93 +#: order/views.py:99 msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:102 order/views.py:149 part/views.py:90 stock/views.py:167 +#: order/views.py:109 order/views.py:157 part/views.py:92 stock/views.py:167 msgid "Added attachment" msgstr "" -#: order/views.py:141 +#: order/views.py:148 msgid "Add Sales Order Attachment" msgstr "" -#: order/views.py:176 order/views.py:197 +#: order/views.py:184 order/views.py:206 msgid "Edit Attachment" msgstr "" -#: order/views.py:180 order/views.py:201 +#: order/views.py:189 order/views.py:211 msgid "Attachment updated" msgstr "" -#: order/views.py:216 order/views.py:230 +#: order/views.py:226 order/views.py:241 msgid "Delete Attachment" msgstr "" -#: order/views.py:222 order/views.py:236 stock/views.py:223 +#: order/views.py:233 order/views.py:248 stock/views.py:223 msgid "Deleted attachment" msgstr "" -#: order/views.py:287 +#: order/views.py:301 msgid "Create Purchase Order" msgstr "" -#: order/views.py:318 +#: order/views.py:333 msgid "Create Sales Order" msgstr "" -#: order/views.py:348 +#: order/views.py:364 msgid "Edit Purchase Order" msgstr "" -#: order/views.py:368 +#: order/views.py:385 msgid "Edit Sales Order" msgstr "" -#: order/views.py:384 +#: order/views.py:402 msgid "Cancel Order" msgstr "" -#: order/views.py:399 order/views.py:431 +#: order/views.py:418 order/views.py:451 msgid "Confirm order cancellation" msgstr "" -#: order/views.py:417 +#: order/views.py:436 msgid "Cancel sales order" msgstr "" -#: order/views.py:437 +#: order/views.py:457 msgid "Could not cancel order" msgstr "" -#: order/views.py:451 +#: order/views.py:471 msgid "Issue Order" msgstr "" -#: order/views.py:466 +#: order/views.py:487 msgid "Confirm order placement" msgstr "" -#: order/views.py:487 +#: order/views.py:508 msgid "Complete Order" msgstr "" -#: order/views.py:522 +#: order/views.py:544 msgid "Ship Order" msgstr "" -#: order/views.py:538 +#: order/views.py:561 msgid "Confirm order shipment" msgstr "" -#: order/views.py:544 +#: order/views.py:567 msgid "Could not ship order" msgstr "" -#: order/views.py:595 +#: order/views.py:619 msgid "Receive Parts" msgstr "" -#: order/views.py:662 +#: order/views.py:687 msgid "Items received" msgstr "" -#: order/views.py:676 +#: order/views.py:701 msgid "No destination set" msgstr "" -#: order/views.py:721 +#: order/views.py:746 msgid "Error converting quantity to number" msgstr "" -#: order/views.py:727 +#: order/views.py:752 msgid "Receive quantity less than zero" msgstr "" -#: order/views.py:733 +#: order/views.py:758 msgid "No lines specified" msgstr "" -#: order/views.py:1107 +#: order/views.py:1138 msgid "Invalid Purchase Order" msgstr "" -#: order/views.py:1115 +#: order/views.py:1146 msgid "Supplier must match for Part and Order" msgstr "" -#: order/views.py:1120 +#: order/views.py:1151 msgid "Invalid SupplierPart selection" msgstr "" -#: order/views.py:1252 order/views.py:1270 +#: order/views.py:1284 order/views.py:1303 msgid "Edit Line Item" msgstr "" -#: order/views.py:1286 order/views.py:1298 +#: order/views.py:1320 order/views.py:1333 msgid "Delete Line Item" msgstr "" -#: order/views.py:1291 order/views.py:1303 +#: order/views.py:1326 order/views.py:1339 msgid "Deleted line item" msgstr "" -#: order/views.py:1312 +#: order/views.py:1348 msgid "Allocate Stock to Order" msgstr "" -#: order/views.py:1381 +#: order/views.py:1418 msgid "Edit Allocation Quantity" msgstr "" -#: order/views.py:1396 +#: order/views.py:1434 msgid "Remove allocation" msgstr "" @@ -2226,7 +2242,7 @@ msgstr "" msgid "BOM line checksum" msgstr "" -#: part/models.py:1612 part/views.py:1349 part/views.py:1401 +#: part/models.py:1612 part/views.py:1351 part/views.py:1403 #: stock/models.py:231 msgid "Quantity must be integer value for trackable parts" msgstr "" @@ -2293,7 +2309,7 @@ msgstr "" msgid "Validate Bill of Materials" msgstr "" -#: part/templates/part/bom.html:48 part/views.py:1640 +#: part/templates/part/bom.html:48 part/views.py:1642 msgid "Export Bill of Materials" msgstr "" @@ -2385,7 +2401,7 @@ msgstr "" msgid "All parts" msgstr "" -#: part/templates/part/category.html:24 part/views.py:2043 +#: part/templates/part/category.html:24 part/views.py:2045 msgid "Create new part category" msgstr "" @@ -2425,7 +2441,7 @@ msgstr "" msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:114 part/views.py:511 +#: part/templates/part/category.html:114 part/views.py:513 msgid "Create new part" msgstr "" @@ -2642,7 +2658,7 @@ msgid "Edit" msgstr "" #: part/templates/part/params.html:39 part/templates/part/supplier.html:17 -#: users/models.py:141 +#: users/models.py:145 msgid "Delete" msgstr "" @@ -2842,176 +2858,176 @@ msgstr "" msgid "New Variant" msgstr "" -#: part/views.py:78 +#: part/views.py:80 msgid "Add part attachment" msgstr "" -#: part/views.py:129 templates/attachment_table.html:30 +#: part/views.py:131 templates/attachment_table.html:30 msgid "Edit attachment" msgstr "" -#: part/views.py:135 +#: part/views.py:137 msgid "Part attachment updated" msgstr "" -#: part/views.py:150 +#: part/views.py:152 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:158 +#: part/views.py:160 msgid "Deleted part attachment" msgstr "" -#: part/views.py:167 +#: part/views.py:169 msgid "Create Test Template" msgstr "" -#: part/views.py:196 +#: part/views.py:198 msgid "Edit Test Template" msgstr "" -#: part/views.py:212 +#: part/views.py:214 msgid "Delete Test Template" msgstr "" -#: part/views.py:221 +#: part/views.py:223 msgid "Set Part Category" msgstr "" -#: part/views.py:271 +#: part/views.py:273 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:306 +#: part/views.py:308 msgid "Create Variant" msgstr "" -#: part/views.py:386 +#: part/views.py:388 msgid "Duplicate Part" msgstr "" -#: part/views.py:393 +#: part/views.py:395 msgid "Copied part" msgstr "" -#: part/views.py:518 +#: part/views.py:520 msgid "Created new part" msgstr "" -#: part/views.py:733 +#: part/views.py:735 msgid "Part QR Code" msgstr "" -#: part/views.py:752 +#: part/views.py:754 msgid "Upload Part Image" msgstr "" -#: part/views.py:760 part/views.py:797 +#: part/views.py:762 part/views.py:799 msgid "Updated part image" msgstr "" -#: part/views.py:769 +#: part/views.py:771 msgid "Select Part Image" msgstr "" -#: part/views.py:800 +#: part/views.py:802 msgid "Part image not found" msgstr "" -#: part/views.py:811 +#: part/views.py:813 msgid "Edit Part Properties" msgstr "" -#: part/views.py:835 +#: part/views.py:837 msgid "Validate BOM" msgstr "" -#: part/views.py:1002 +#: part/views.py:1004 msgid "No BOM file provided" msgstr "" -#: part/views.py:1352 +#: part/views.py:1354 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1377 part/views.py:1380 +#: part/views.py:1379 part/views.py:1382 msgid "Select valid part" msgstr "" -#: part/views.py:1386 +#: part/views.py:1388 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1424 +#: part/views.py:1426 msgid "Select a part" msgstr "" -#: part/views.py:1430 +#: part/views.py:1432 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1434 +#: part/views.py:1436 msgid "Specify quantity" msgstr "" -#: part/views.py:1690 +#: part/views.py:1692 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1699 +#: part/views.py:1701 msgid "Part was deleted" msgstr "" -#: part/views.py:1708 +#: part/views.py:1710 msgid "Part Pricing" msgstr "" -#: part/views.py:1834 +#: part/views.py:1836 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1844 +#: part/views.py:1846 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1853 +#: part/views.py:1855 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1863 +#: part/views.py:1865 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1915 +#: part/views.py:1917 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:1931 +#: part/views.py:1933 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1990 +#: part/views.py:1992 msgid "Edit Part Category" msgstr "" -#: part/views.py:2027 +#: part/views.py:2029 msgid "Delete Part Category" msgstr "" -#: part/views.py:2035 +#: part/views.py:2037 msgid "Part category was deleted" msgstr "" -#: part/views.py:2098 +#: part/views.py:2100 msgid "Create BOM item" msgstr "" -#: part/views.py:2166 +#: part/views.py:2168 msgid "Edit BOM item" msgstr "" -#: part/views.py:2216 +#: part/views.py:2218 msgid "Confim BOM item deletion" msgstr "" @@ -4497,58 +4513,58 @@ msgstr "" msgid "Delete Stock" msgstr "" -#: users/admin.py:62 +#: users/admin.py:61 msgid "Users" msgstr "" -#: users/admin.py:63 +#: users/admin.py:62 msgid "Select which users are assigned to this group" msgstr "" -#: users/admin.py:124 +#: users/admin.py:120 msgid "Personal info" msgstr "" -#: users/admin.py:125 +#: users/admin.py:121 msgid "Permissions" msgstr "" -#: users/admin.py:128 +#: users/admin.py:124 msgid "Important dates" msgstr "" -#: users/models.py:124 +#: users/models.py:128 msgid "Permission set" msgstr "" -#: users/models.py:132 +#: users/models.py:136 msgid "Group" msgstr "" -#: users/models.py:135 +#: users/models.py:139 msgid "View" msgstr "" -#: users/models.py:135 +#: users/models.py:139 msgid "Permission to view items" msgstr "" -#: users/models.py:137 -msgid "Create" -msgstr "" - -#: users/models.py:137 -msgid "Permission to add items" -msgstr "" - -#: users/models.py:139 -msgid "Update" -msgstr "" - -#: users/models.py:139 -msgid "Permissions to edit items" +#: users/models.py:141 +msgid "Add" msgstr "" #: users/models.py:141 +msgid "Permission to add items" +msgstr "" + +#: users/models.py:143 +msgid "Change" +msgstr "" + +#: users/models.py:143 +msgid "Permissions to edit items" +msgstr "" + +#: users/models.py:145 msgid "Permission to delete items" msgstr ""