2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into plugin-2037

This commit is contained in:
Matthias 2021-11-18 11:37:11 +01:00
commit a95b298c62
No known key found for this signature in database
GPG Key ID: F50EF5741D33E076
25 changed files with 409 additions and 93 deletions

View File

@ -92,6 +92,12 @@ DEBUG = _is_true(get_setting(
CONFIG.get('debug', True) CONFIG.get('debug', True)
)) ))
# Determine if we are running in "demo mode"
DEMO_MODE = _is_true(get_setting(
'INVENTREE_DEMO',
CONFIG.get('demo', False)
))
DOCKER = _is_true(get_setting( DOCKER = _is_true(get_setting(
'INVENTREE_DOCKER', 'INVENTREE_DOCKER',
False False
@ -239,7 +245,10 @@ STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
if DEBUG: if DEBUG:
logger.info("InvenTree running in DEBUG mode") logger.info("InvenTree running with DEBUG enabled")
if DEMO_MODE:
logger.warning("InvenTree running in DEMO mode")
logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'") logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'") logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -1,13 +0,0 @@
from rest_framework.views import exception_handler
def api_exception_handler(exc, context):
response = exception_handler(exc, context)
# Now add the HTTP status code to the response.
if response is not None:
data = {'error': response.data}
response.data = data
return response

View File

@ -12,11 +12,16 @@ import common.models
INVENTREE_SW_VERSION = "0.6.0 dev" INVENTREE_SW_VERSION = "0.6.0 dev"
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 17 INVENTREE_API_VERSION = 18
""" """
Increment this API version number whenever there is a significant change to the API that any clients need to know about Increment this API version number whenever there is a significant change to the API that any clients need to know about
v18 -> 2021-11-15
- Adds the ability to filter BomItem API by "uses" field
- This returns a list of all BomItems which "use" the specified part
- Includes inherited BomItem objects
v17 -> 2021-11-09 v17 -> 2021-11-09
- Adds API endpoints for GLOBAL and USER settings objects - Adds API endpoints for GLOBAL and USER settings objects
- Ref: https://github.com/inventree/InvenTree/pull/2275 - Ref: https://github.com/inventree/InvenTree/pull/2275

View File

@ -832,18 +832,6 @@ class PartList(generics.ListCreateAPIView):
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
# Filter by "uses" query - Limit to parts which use the provided part
uses = params.get('uses', None)
if uses:
try:
uses = Part.objects.get(pk=uses)
queryset = queryset.filter(uses.get_used_in_filter())
except (ValueError, Part.DoesNotExist):
pass
# Exclude specific part ID values? # Exclude specific part ID values?
exclude_id = [] exclude_id = []
@ -1040,13 +1028,19 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
serializer_class = part_serializers.PartParameterTemplateSerializer serializer_class = part_serializers.PartParameterTemplateSerializer
filter_backends = [ filter_backends = [
DjangoFilterBackend,
filters.OrderingFilter, filters.OrderingFilter,
filters.SearchFilter,
] ]
filter_fields = [ filter_fields = [
'name', 'name',
] ]
search_fields = [
'name',
]
class PartParameterList(generics.ListCreateAPIView): class PartParameterList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartParameter objects """ API endpoint for accessing a list of PartParameter objects
@ -1211,6 +1205,54 @@ class BomList(generics.ListCreateAPIView):
except (ValueError, Part.DoesNotExist): except (ValueError, Part.DoesNotExist):
pass pass
"""
Filter by 'uses'?
Here we pass a part ID and return BOM items for any assemblies which "use" (or "require") that part.
There are multiple ways that an assembly can "use" a sub-part:
A) Directly specifying the sub_part in a BomItem field
B) Specifing a "template" part with inherited=True
C) Allowing variant parts to be substituted
D) Allowing direct substitute parts to be specified
- BOM items which are "inherited" by parts which are variants of the master BomItem
"""
uses = params.get('uses', None)
if uses is not None:
try:
# Extract the part we are interested in
uses_part = Part.objects.get(pk=uses)
# Construct the database query in multiple parts
# A) Direct specification of sub_part
q_A = Q(sub_part=uses_part)
# B) BomItem is inherited and points to a "parent" of this part
parents = uses_part.get_ancestors(include_self=False)
q_B = Q(
inherited=True,
sub_part__in=parents
)
# C) Substitution of variant parts
# TODO
# D) Specification of individual substitutes
# TODO
q = q_A | q_B
queryset = queryset.filter(q)
except (ValueError, Part.DoesNotExist):
pass
if self.include_pricing(): if self.include_pricing():
queryset = self.annotate_pricing(queryset) queryset = self.annotate_pricing(queryset)

View File

@ -1,13 +1,29 @@
# Generated by Django 2.2 on 2019-05-20 12:04 # Generated by Django 2.2 on 2019-05-20 12:04
import InvenTree.validators import os
from django.conf import settings
import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings
import django.core.validators
import InvenTree.validators
import part.models import part.models
def attach_file(instance, filename):
"""
Generate a filename for the uploaded attachment.
2021-11-17 - This was moved here from part.models.py,
as the function itself is no longer used,
but is still required for migration
"""
# Construct a path to store a file attachment
return os.path.join('part_files', str(instance.part.id), filename)
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
@ -61,7 +77,7 @@ class Migration(migrations.Migration):
name='PartAttachment', name='PartAttachment',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attachment', models.FileField(help_text='Select file to attach', upload_to=part.models.attach_file)), ('attachment', models.FileField(help_text='Select file to attach', upload_to=attach_file)),
('comment', models.CharField(help_text='File comment', max_length=100)), ('comment', models.CharField(help_text='File comment', max_length=100)),
], ],
), ),

View File

@ -2135,20 +2135,6 @@ def after_save_part(sender, instance: Part, created, **kwargs):
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance) InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance)
def attach_file(instance, filename):
""" Function for storing a file for a PartAttachment
Args:
instance: Instance of a PartAttachment object
filename: name of uploaded file
Returns:
path to store file, format: 'part_file_<pk>_filename'
"""
# Construct a path to store a file attachment
return os.path.join('part_files', str(instance.part.id), filename)
class PartAttachment(InvenTreeAttachment): class PartAttachment(InvenTreeAttachment):
""" """
Model for storing file attachments against a Part object Model for storing file attachments against a Part object

View File

@ -388,9 +388,7 @@
{% if part.variant_of %} {% if part.variant_of %}
<li><a class='dropdown-item' href='#' id='bom-duplicate'><span class='fas fa-clone'></span> {% trans "Copy BOM" %}</a></li> <li><a class='dropdown-item' href='#' id='bom-duplicate'><span class='fas fa-clone'></span> {% trans "Copy BOM" %}</a></li>
{% endif %} {% endif %}
{% if not part.is_bom_valid %}
<li><a class='dropdown-item' href='#' id='validate-bom'><span class='fas fa-clipboard-check icon-green'></span> {% trans "Validate BOM" %}</a></li> <li><a class='dropdown-item' href='#' id='validate-bom'><span class='fas fa-clipboard-check icon-green'></span> {% trans "Validate BOM" %}</a></li>
{% endif %}
</ul> </ul>
</div> </div>
@ -649,14 +647,10 @@
// Load the "used in" tab // Load the "used in" tab
onPanelLoad("used-in", function() { onPanelLoad("used-in", function() {
loadPartTable('#used-table',
'{% url "api-part-list" %}', loadUsedInTable(
{ '#used-table',
params: { {{ part.pk }},
uses: {{ part.pk }},
},
filterTarget: '#filter-list-usedin',
}
); );
}); });

View File

@ -90,6 +90,13 @@ def inventree_in_debug_mode(*args, **kwargs):
return djangosettings.DEBUG return djangosettings.DEBUG
@register.simple_tag()
def inventree_demo_mode(*args, **kwargs):
""" Return True if the server is running in DEMO mode """
return djangosettings.DEMO_MODE
@register.simple_tag() @register.simple_tag()
def inventree_docker_mode(*args, **kwargs): def inventree_docker_mode(*args, **kwargs):
""" Return True if the server is running as a Docker image """ """ Return True if the server is running as a Docker image """

View File

@ -1123,6 +1123,59 @@ class BomItemTest(InvenTreeAPITestCase):
response = self.get(url, expected_code=200) response = self.get(url, expected_code=200)
self.assertEqual(len(response.data), 5) self.assertEqual(len(response.data), 5)
def test_bom_item_uses(self):
"""
Tests for the 'uses' field
"""
url = reverse('api-bom-list')
# Test that the direct 'sub_part' association works
assemblies = []
for i in range(5):
assy = Part.objects.create(
name=f"Assy_{i}",
description="An assembly made of other parts",
active=True,
assembly=True
)
assemblies.append(assy)
components = []
# Create some sub-components
for i in range(5):
cmp = Part.objects.create(
name=f"Component_{i}",
description="A sub component",
active=True,
component=True
)
for j in range(i):
# Create a BOM item
BomItem.objects.create(
quantity=10,
part=assemblies[j],
sub_part=cmp,
)
components.append(cmp)
response = self.get(
url,
{
'uses': cmp.pk,
},
expected_code=200,
)
self.assertEqual(len(response.data), i)
class PartParameterTest(InvenTreeAPITestCase): class PartParameterTest(InvenTreeAPITestCase):
""" """

View File

@ -12,12 +12,15 @@
{% endblock %} {% endblock %}
{% block actions %} {% block actions %}
{% inventree_demo_mode as demo %}
{% if not demo %}
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'> <div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
<span class='fas fa-user-cog'></span> {% trans "Edit" %} <span class='fas fa-user-cog'></span> {% trans "Edit" %}
</div> </div>
<div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'> <div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
<span class='fas fa-key'></span> {% trans "Set Password" %} <span class='fas fa-key'></span> {% trans "Set Password" %}
</div> </div>
{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -1,5 +1,6 @@
{% extends "account/base.html" %} {% extends "account/base.html" %}
{% load inventree_extras %}
{% load i18n account socialaccount crispy_forms_tags inventree_extras %} {% load i18n account socialaccount crispy_forms_tags inventree_extras %}
{% block head_title %}{% trans "Sign In" %}{% endblock %} {% block head_title %}{% trans "Sign In" %}{% endblock %}
@ -10,6 +11,7 @@
{% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %} {% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %}
{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %} {% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %}
{% mail_configured as mail_conf %} {% mail_configured as mail_conf %}
{% inventree_demo_mode as demo %}
<h1>{% trans "Sign In" %}</h1> <h1>{% trans "Sign In" %}</h1>
@ -36,9 +38,16 @@ for a account and sign in below:{% endblocktrans %}</p>
<div class="btn-group float-right" role="group"> <div class="btn-group float-right" role="group">
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button> <button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
</div> </div>
{% if mail_conf and enable_pwd_forgot %} {% if mail_conf and enable_pwd_forgot and not demo %}
<a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a> <a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a>
{% endif %} {% endif %}
{% if demo %}
<p>
<h6>
{% trans "InvenTree demo instance" %} - <a href='https://inventree.readthedocs.io/en/latest/demo/'>{% trans "Click here for login details" %}</a>
</h6>
</p>
{% endif %}
</form> </form>
{% if enable_sso %} {% if enable_sso %}

View File

@ -87,24 +87,19 @@
</div> </div>
<main class='col ps-md-2 pt-2 pe-2'> <main class='col ps-md-2 pt-2 pe-2'>
{% block alerts %}
<div class='notification-area' id='alerts'>
<!-- Div for displayed alerts -->
{% if server_restart_required %} {% if server_restart_required %}
<div class='notification-area' id='restart-required'>
<div id='alert-restart-server' class='alert alert-danger' role='alert'> <div id='alert-restart-server' class='alert alert-danger' role='alert'>
<span class='fas fa-server'></span> <span class='fas fa-server'></span>
<b>{% trans "Server Restart Required" %}</b> <b>{% trans "Server Restart Required" %}</b>
<small> <small>
<br> <br>
{% trans "A configuration option has been changed which requires a server restart" %}. {% trans "A configuration option has been changed which requires a server restart" %}. {% trans "Contact your system administrator for further information" %}
<br>
{% trans "Contact your system administrator for further information" %}
</small> </small>
</div> </div>
</div>
{% endif %} {% endif %}
{% block alerts %}
<div class='notification-area' id='alerts'>
<!-- Div for displayed alerts -->
</div> </div>
{% endblock %} {% endblock %}

View File

@ -217,8 +217,10 @@ function showApiError(xhr, url) {
break; break;
} }
if (url) {
message += '<hr>'; message += '<hr>';
message += `URL: ${url}`; message += `URL: ${url}`;
}
showMessage(title, { showMessage(title, {
style: 'danger', style: 'danger',

View File

@ -16,6 +16,7 @@
/* exported /* exported
newPartFromBomWizard, newPartFromBomWizard,
loadBomTable, loadBomTable,
loadUsedInTable,
removeRowFromBomWizard, removeRowFromBomWizard,
removeColFromBomWizard, removeColFromBomWizard,
*/ */
@ -311,7 +312,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
} }
function loadBomTable(table, options) { function loadBomTable(table, options={}) {
/* Load a BOM table with some configurable options. /* Load a BOM table with some configurable options.
* *
* Following options are available: * Following options are available:
@ -395,7 +396,7 @@ function loadBomTable(table, options) {
var sub_part = row.sub_part_detail; var sub_part = row.sub_part_detail;
html += makePartIcons(row.sub_part_detail); html += makePartIcons(sub_part);
if (row.substitutes && row.substitutes.length > 0) { if (row.substitutes && row.substitutes.length > 0) {
html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}'); html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}');
@ -672,8 +673,9 @@ function loadBomTable(table, options) {
table.treegrid('collapseAll'); table.treegrid('collapseAll');
}, },
error: function() { error: function(xhr) {
console.log('Error requesting BOM for part=' + part_pk); console.log('Error requesting BOM for part=' + part_pk);
showApiError(xhr);
} }
} }
); );
@ -835,3 +837,166 @@ function loadBomTable(table, options) {
}); });
} }
} }
/*
* Load a table which shows the assemblies which "require" a certain part.
*
* Arguments:
* - table: The ID string of the table element e.g. '#used-in-table'
* - part_id: The ID (PK) of the part we are interested in
*
* Options:
* -
*
* The following "options" are available.
*/
function loadUsedInTable(table, part_id, options={}) {
var params = options.params || {};
params.uses = part_id;
params.part_detail = true;
params.sub_part_detail = true,
params.show_pricing = global_settings.PART_SHOW_PRICE_IN_BOM;
var filters = {};
if (!options.disableFilters) {
filters = loadTableFilters('usedin');
}
for (var key in params) {
filters[key] = params[key];
}
setupFilterList('usedin', $(table), options.filterTarget || '#filter-list-usedin');
function loadVariantData(row) {
// Load variants information for inherited BOM rows
inventreeGet(
'{% url "api-part-list" %}',
{
assembly: true,
ancestor: row.part,
},
{
success: function(variantData) {
// Iterate through each variant item
for (var jj = 0; jj < variantData.length; jj++) {
variantData[jj].parent = row.pk;
var variant = variantData[jj];
// Add this variant to the table, augmented
$(table).bootstrapTable('append', [{
// Point the parent to the "master" assembly row
parent: row.pk,
part: variant.pk,
part_detail: variant,
sub_part: row.sub_part,
sub_part_detail: row.sub_part_detail,
quantity: row.quantity,
}]);
}
},
error: function(xhr) {
showApiError(xhr);
}
}
);
}
$(table).inventreeTable({
url: options.url || '{% url "api-bom-list" %}',
name: options.table_name || 'usedin',
sortable: true,
search: true,
showColumns: true,
queryParams: filters,
original: params,
rootParentId: 'top-level-item',
idField: 'pk',
uniqueId: 'pk',
parentIdField: 'parent',
treeShowField: 'part',
onLoadSuccess: function(tableData) {
// Once the initial data are loaded, check if there are any "inherited" BOM lines
for (var ii = 0; ii < tableData.length; ii++) {
var row = tableData[ii];
// This is a "top level" item in the table
row.parent = 'top-level-item';
// Ignore this row as it is not "inherited" by variant parts
if (!row.inherited) {
continue;
}
loadVariantData(row);
}
},
onPostBody: function() {
$(table).treegrid({
treeColumn: 0,
});
},
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'part',
title: '{% trans "Assembly" %}',
switchable: false,
sortable: true,
formatter: function(value, row) {
var url = `/part/${value}/?display=bom`;
var html = '';
var part = row.part_detail;
html += imageHoverIcon(part.thumbnail);
html += renderLink(part.full_name, url);
html += makePartIcons(part);
return html;
}
},
{
field: 'sub_part',
title: '{% trans "Required Part" %}',
sortable: true,
formatter: function(value, row) {
var url = `/part/${value}/`;
var html = '';
var sub_part = row.sub_part_detail;
html += imageHoverIcon(sub_part.thumbnail);
html += renderLink(sub_part.full_name, url);
html += makePartIcons(sub_part);
return html;
}
},
{
field: 'quantity',
title: '{% trans "Required Quantity" %}',
formatter: function(value, row) {
var html = value;
if (row.parent && row.parent != 'top-level-item') {
html += ` <em>({% trans "Inherited from parent BOM" %})</em>`;
}
return html;
}
}
]
});
}

View File

@ -281,23 +281,24 @@ function setupFilterList(tableKey, table, target) {
// One blank slate, please // One blank slate, please
element.empty(); element.empty();
element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`); var buttons = '';
// Callback for reloading the table buttons += `<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`;
element.find(`#reload-${tableKey}`).click(function() {
$(table).bootstrapTable('refresh');
});
// If there are no filters defined for this table, exit now // If there are filters defined for this table, add more buttons
if (jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) { if (!jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) {
return; buttons += `<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-filter'></span></button>`;
}
element.append(`<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-filter'></span></button>`);
if (Object.keys(filters).length > 0) { if (Object.keys(filters).length > 0) {
element.append(`<button id='${clear}' title='{% trans "Clear all filters" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-backspace icon-red'></span></button>`); buttons += `<button id='${clear}' title='{% trans "Clear all filters" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-backspace icon-red'></span></button>`;
} }
}
element.html(`
<div class='btn-group' role='group'>
${buttons}
</div>
`);
for (var key in filters) { for (var key in filters) {
var value = getFilterOptionValue(tableKey, key, filters[key]); var value = getFilterOptionValue(tableKey, key, filters[key]);
@ -307,6 +308,11 @@ function setupFilterList(tableKey, table, target) {
element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`); element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
} }
// Callback for reloading the table
element.find(`#reload-${tableKey}`).click(function() {
$(table).bootstrapTable('refresh');
});
// Add a callback for adding a new filter // Add a callback for adding a new filter
element.find(`#${add}`).click(function clicked() { element.find(`#${add}`).click(function clicked() {
@ -316,10 +322,12 @@ function setupFilterList(tableKey, table, target) {
var html = ''; var html = '';
html += `<div class='input-group'>`;
html += generateAvailableFilterList(tableKey); html += generateAvailableFilterList(tableKey);
html += generateFilterInput(tableKey); html += generateFilterInput(tableKey);
html += `<button title='{% trans "Create filter" %}' class='btn btn-outline-secondary filter-button' id='${make}'><span class='fas fa-plus'></span></button>`; html += `<button title='{% trans "Create filter" %}' class='btn btn-outline-secondary filter-button' id='${make}'><span class='fas fa-plus'></span></button>`;
html += `</div>`;
element.append(html); element.append(html);

View File

@ -924,8 +924,8 @@ function handleFormSuccess(response, options) {
var cache = (options.follow && response.url) || options.redirect || options.reload; var cache = (options.follow && response.url) || options.redirect || options.reload;
// Display any messages // Display any messages
if (response && response.success) { if (response && (response.success || options.successMessage)) {
showAlertOrCache(response.success, cache, {style: 'success'}); showAlertOrCache(response.success || options.successMessage, cache, {style: 'success'});
} }
if (response && response.info) { if (response && response.info) {

View File

@ -331,6 +331,7 @@ function editPart(pk) {
groups: groups, groups: groups,
title: '{% trans "Edit Part" %}', title: '{% trans "Edit Part" %}',
reload: true, reload: true,
successMessage: '{% trans "Part edited" %}',
}); });
} }

View File

@ -77,10 +77,22 @@ function getAvailableTableFilters(tableKey) {
// Filters for the "used in" table // Filters for the "used in" table
if (tableKey == 'usedin') { if (tableKey == 'usedin') {
return { return {
'inherited': {
type: 'bool',
title: '{% trans "Inherited" %}',
},
'optional': {
type: 'bool',
title: '{% trans "Optional" %}',
},
'part_active': { 'part_active': {
type: 'bool', type: 'bool',
title: '{% trans "Active" %}', title: '{% trans "Active" %}',
}, },
'part_trackable': {
type: 'bool',
title: '{% trans "Trackable" %}',
},
}; };
} }

View File

@ -6,6 +6,7 @@
{% settings_value 'BARCODE_ENABLE' as barcodes %} {% settings_value 'BARCODE_ENABLE' as barcodes %}
{% settings_value 'STICKY_HEADER' user=request.user as sticky %} {% settings_value 'STICKY_HEADER' user=request.user as sticky %}
{% navigation_enabled as plugin_nav %} {% navigation_enabled as plugin_nav %}
{% inventree_demo_mode as demo %}
<nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light"> <nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light">
<div class="container-fluid"> <div class="container-fluid">
@ -83,6 +84,9 @@
</ul> </ul>
</div> </div>
{% if demo %}
{% include "navbar_demo.html" %}
{% endif %}
{% include "search_form.html" %} {% include "search_form.html" %}
<ul class='navbar-nav flex-row'> <ul class='navbar-nav flex-row'>
{% if barcodes %} {% if barcodes %}
@ -103,7 +107,7 @@
</a> </a>
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'> <ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if user.is_staff %} {% if user.is_staff and not demo %}
<li><a class='dropdown-item' href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li> <li><a class='dropdown-item' href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li>
{% endif %} {% endif %}
<li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li> <li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>

View File

@ -0,0 +1,12 @@
{% load i18n %}
{% include "spacer.html" %}
<div class='flex'>
<h6>
{% trans "InvenTree demo mode" %}
<a href='https://inventree.readthedocs.io/en/latest/demo/'>
<span class='fas fa-info-circle'></span>
</a>
</h6>
</div>
{% include "spacer.html" %}
{% include "spacer.html" %}

View File

@ -2,8 +2,10 @@
<form class="d-flex" action="{% url 'search' %}" method='post'> <form class="d-flex" action="{% url 'search' %}" method='post'>
{% csrf_token %} {% csrf_token %}
<div class='input-group'>
<input type="text" name='search' class="form-control" aria-label='{% trans "Search" %}' id="search-bar" placeholder="{% trans 'Search' %}"{% if query_text %} value="{{ query }}"{% endif %}> <input type="text" name='search' class="form-control" aria-label='{% trans "Search" %}' id="search-bar" placeholder="{% trans 'Search' %}"{% if query_text %} value="{{ query }}"{% endif %}>
<button type="submit" id='search-submit' class="btn btn-secondary" title='{% trans "Search" %}'> <button type="submit" id='search-submit' class="btn btn-secondary" title='{% trans "Search" %}'>
<span class='fas fa-search'></span> <span class='fas fa-search'></span>
</button> </button>
</div>
</form> </form>

View File

@ -27,6 +27,9 @@ function {{ label }}StatusDisplay(key, options={}) {
label = {{ label }}Codes[key].label; label = {{ label }}Codes[key].label;
} }
// Fallback option for label
label = label || 'bg-dark';
if (value == null || value.length == 0) { if (value == null || value.length == 0) {
value = key; value = key;
label = ''; label = '';

View File

@ -30,6 +30,7 @@ flake8==3.8.3 # PEP checking
gunicorn>=20.1.0 # Gunicorn web server gunicorn>=20.1.0 # Gunicorn web server
importlib_metadata # Backport for importlib.metadata importlib_metadata # Backport for importlib.metadata
inventree # Install the latest version of the InvenTree API python library inventree # Install the latest version of the InvenTree API python library
markdown==3.3.4 # Force particular version of markdown
pep8-naming==0.11.1 # PEP naming convention extension pep8-naming==0.11.1 # PEP naming convention extension
pillow==8.3.2 # Image manipulation pillow==8.3.2 # Image manipulation
py-moneyed==0.8.0 # Specific version requirement for py-moneyed py-moneyed==0.8.0 # Specific version requirement for py-moneyed