diff --git a/InvenTree/templates/attachment_button.html b/InvenTree/templates/attachment_button.html
index e1561010c0..d220f4829d 100644
--- a/InvenTree/templates/attachment_button.html
+++ b/InvenTree/templates/attachment_button.html
@@ -1,5 +1,8 @@
{% load i18n %}
+
\ No newline at end of file
diff --git a/InvenTree/templates/js/translated/api.js b/InvenTree/templates/js/translated/api.js
index 735ce0a676..d9c23f035f 100644
--- a/InvenTree/templates/js/translated/api.js
+++ b/InvenTree/templates/js/translated/api.js
@@ -54,6 +54,7 @@ function inventreeGet(url, filters={}, options={}) {
data: filters,
dataType: 'json',
contentType: 'application/json',
+ async: (options.async == false) ? false : true,
success: function(response) {
if (options.success) {
options.success(response);
diff --git a/InvenTree/templates/js/translated/attachment.js b/InvenTree/templates/js/translated/attachment.js
index 5ff5786588..5c5af5682f 100644
--- a/InvenTree/templates/js/translated/attachment.js
+++ b/InvenTree/templates/js/translated/attachment.js
@@ -6,10 +6,57 @@
*/
/* exported
+ addAttachmentButtonCallbacks,
loadAttachmentTable,
reloadAttachmentTable,
*/
+
+/*
+ * Add callbacks to buttons for creating new attachments.
+ *
+ * Note: Attachments can also be external links!
+ */
+function addAttachmentButtonCallbacks(url, fields={}) {
+
+ // Callback for 'new attachment' button
+ $('#new-attachment').click(function() {
+
+ var file_fields = {
+ attachment: {},
+ comment: {},
+ };
+
+ Object.assign(file_fields, fields);
+
+ constructForm(url, {
+ fields: file_fields,
+ method: 'POST',
+ onSuccess: reloadAttachmentTable,
+ title: '{% trans "Add Attachment" %}',
+ });
+ });
+
+ // Callback for 'new link' button
+ $('#new-attachment-link').click(function() {
+
+ var link_fields = {
+ link: {},
+ comment: {},
+ };
+
+ Object.assign(link_fields, fields);
+
+ constructForm(url, {
+ fields: link_fields,
+ method: 'POST',
+ onSuccess: reloadAttachmentTable,
+ title: '{% trans "Add Link" %}',
+ });
+ });
+}
+
+
function reloadAttachmentTable() {
$('#attachment-table').bootstrapTable('refresh');
@@ -20,6 +67,8 @@ function loadAttachmentTable(url, options) {
var table = options.table || '#attachment-table';
+ addAttachmentButtonCallbacks(url, options.fields || {});
+
$(table).inventreeTable({
url: url,
name: options.name || 'attachments',
@@ -34,56 +83,77 @@ function loadAttachmentTable(url, options) {
$(table).find('.button-attachment-edit').click(function() {
var pk = $(this).attr('pk');
- if (options.onEdit) {
- options.onEdit(pk);
- }
+ constructForm(`${url}${pk}/`, {
+ fields: {
+ link: {},
+ comment: {},
+ },
+ processResults: function(data, fields, opts) {
+ // Remove the "link" field if the attachment is a file!
+ if (data.attachment) {
+ delete opts.fields.link;
+ }
+ },
+ onSuccess: reloadAttachmentTable,
+ title: '{% trans "Edit Attachment" %}',
+ });
});
// Add callback for 'delete' button
$(table).find('.button-attachment-delete').click(function() {
var pk = $(this).attr('pk');
- if (options.onDelete) {
- options.onDelete(pk);
- }
+ constructForm(`${url}${pk}/`, {
+ method: 'DELETE',
+ confirmMessage: '{% trans "Confirm Delete" %}',
+ title: '{% trans "Delete Attachment" %}',
+ onSuccess: reloadAttachmentTable,
+ });
});
},
columns: [
{
field: 'attachment',
- title: '{% trans "File" %}',
- formatter: function(value) {
+ title: '{% trans "Attachment" %}',
+ formatter: function(value, row) {
- var icon = 'fa-file-alt';
+ if (row.attachment) {
+ var icon = 'fa-file-alt';
- var fn = value.toLowerCase();
+ var fn = value.toLowerCase();
- if (fn.endsWith('.csv')) {
- icon = 'fa-file-csv';
- } else if (fn.endsWith('.pdf')) {
- icon = 'fa-file-pdf';
- } else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) {
- icon = 'fa-file-excel';
- } else if (fn.endsWith('.doc') || fn.endsWith('.docx')) {
- icon = 'fa-file-word';
- } else if (fn.endsWith('.zip') || fn.endsWith('.7z')) {
- icon = 'fa-file-archive';
+ if (fn.endsWith('.csv')) {
+ icon = 'fa-file-csv';
+ } else if (fn.endsWith('.pdf')) {
+ icon = 'fa-file-pdf';
+ } else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) {
+ icon = 'fa-file-excel';
+ } else if (fn.endsWith('.doc') || fn.endsWith('.docx')) {
+ icon = 'fa-file-word';
+ } else if (fn.endsWith('.zip') || fn.endsWith('.7z')) {
+ icon = 'fa-file-archive';
+ } else {
+ var images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif'];
+
+ images.forEach(function(suffix) {
+ if (fn.endsWith(suffix)) {
+ icon = 'fa-file-image';
+ }
+ });
+ }
+
+ var split = value.split('/');
+ var filename = split[split.length - 1];
+
+ var html = `
${filename}`;
+
+ return renderLink(html, value);
+ } else if (row.link) {
+ var html = `
${row.link}`;
+ return renderLink(html, row.link);
} else {
- var images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif'];
-
- images.forEach(function(suffix) {
- if (fn.endsWith(suffix)) {
- icon = 'fa-file-image';
- }
- });
+ return '-';
}
-
- var split = value.split('/');
- var filename = split[split.length - 1];
-
- var html = `
${filename}`;
-
- return renderLink(html, value);
}
},
{
diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js
index 1885624dd8..3cde5bca61 100644
--- a/InvenTree/templates/js/translated/bom.js
+++ b/InvenTree/templates/js/translated/bom.js
@@ -192,6 +192,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
${part.description} |
+
${part.stock} |
${buttons} |
`;
@@ -212,6 +213,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
{% trans "Part" %} |
{% trans "Description" %} |
+ {% trans "Stock" %} |
|
diff --git a/InvenTree/templates/js/translated/company.js b/InvenTree/templates/js/translated/company.js
index 0e71ad99b1..4ab7a7fa3d 100644
--- a/InvenTree/templates/js/translated/company.js
+++ b/InvenTree/templates/js/translated/company.js
@@ -124,6 +124,7 @@ function supplierPartFields() {
part_detail: true,
manufacturer_detail: true,
},
+ auto_fill: true,
},
description: {},
link: {
diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js
index 227fbb8009..7ae8c3e4b4 100644
--- a/InvenTree/templates/js/translated/filters.js
+++ b/InvenTree/templates/js/translated/filters.js
@@ -273,7 +273,7 @@ function setupFilterList(tableKey, table, target) {
var element = $(target);
- if (!element) {
+ if (!element || !element.exists()) {
console.log(`WARNING: setupFilterList could not find target '${target}'`);
return;
}
diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js
index fd1668cc77..5af85d382e 100644
--- a/InvenTree/templates/js/translated/forms.js
+++ b/InvenTree/templates/js/translated/forms.js
@@ -28,6 +28,7 @@
disableFormInput,
enableFormInput,
hideFormInput,
+ setFormInputPlaceholder,
setFormGroupVisibility,
showFormInput,
*/
@@ -1276,6 +1277,11 @@ function initializeGroups(fields, options) {
}
}
+// Set the placeholder value for a field
+function setFormInputPlaceholder(name, placeholder, options) {
+ $(options.modal).find(`#id_${name}`).attr('placeholder', placeholder);
+}
+
// Clear a form input
function clearFormInput(name, options) {
updateFieldValue(name, null, {}, options);
diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js
index b0e3009720..c61722858d 100644
--- a/InvenTree/templates/js/translated/order.js
+++ b/InvenTree/templates/js/translated/order.js
@@ -695,6 +695,23 @@ function loadPurchaseOrderTable(table, options) {
title: '{% trans "Items" %}',
sortable: true,
},
+ {
+ field: 'responsible',
+ title: '{% trans "Responsible" %}',
+ switchable: true,
+ sortable: false,
+ formatter: function(value, row) {
+ var html = row.responsible_detail.name;
+
+ if (row.responsible_detail.label == 'group') {
+ html += `
`;
+ } else {
+ html += `
`;
+ }
+
+ return html;
+ }
+ },
],
});
}
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index 89e09a314e..1bf025b629 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -32,6 +32,7 @@
loadPartTable,
loadPartTestTemplateTable,
loadPartVariantTable,
+ loadRelatedPartsTable,
loadSellPricingChart,
loadSimplePartTable,
loadStockPricingChart,
@@ -705,6 +706,97 @@ function loadPartParameterTable(table, url, options) {
}
+function loadRelatedPartsTable(table, part_id, options={}) {
+ /*
+ * Load table of "related" parts
+ */
+
+ options.params = options.params || {};
+
+ options.params.part = part_id;
+
+ var filters = {};
+
+ for (var key in options.params) {
+ filters[key] = options.params[key];
+ }
+
+ setupFilterList('related', $(table), options.filterTarget);
+
+ function getPart(row) {
+ if (row.part_1 == part_id) {
+ return row.part_2_detail;
+ } else {
+ return row.part_1_detail;
+ }
+ }
+
+ var columns = [
+ {
+ field: 'name',
+ title: '{% trans "Part" %}',
+ switchable: false,
+ formatter: function(value, row) {
+
+ var part = getPart(row);
+
+ var html = imageHoverIcon(part.thumbnail) + renderLink(part.full_name, `/part/${part.pk}/`);
+
+ html += makePartIcons(part);
+
+ return html;
+ }
+ },
+ {
+ field: 'description',
+ title: '{% trans "Description" %}',
+ formatter: function(value, row) {
+ return getPart(row).description;
+ }
+ },
+ {
+ field: 'actions',
+ title: '',
+ switchable: false,
+ formatter: function(value, row) {
+
+ var html = `
`;
+
+ html += makeIconButton('fa-trash-alt icon-red', 'button-related-delete', row.pk, '{% trans "Delete part relationship" %}');
+
+ html += '
';
+
+ return html;
+ }
+ }
+ ];
+
+ $(table).inventreeTable({
+ url: '{% url "api-part-related-list" %}',
+ groupBy: false,
+ name: 'related',
+ original: options.params,
+ queryParams: filters,
+ columns: columns,
+ showColumns: false,
+ search: true,
+ onPostBody: function() {
+ $(table).find('.button-related-delete').click(function() {
+ var pk = $(this).attr('pk');
+
+ constructForm(`/api/part/related/${pk}/`, {
+ method: 'DELETE',
+ title: '{% trans "Delete Part Relationship" %}',
+ onSuccess: function() {
+ $(table).bootstrapTable('refresh');
+ }
+ });
+ });
+ },
+ });
+}
+
+
function loadParametricPartTable(table, options={}) {
/* Load parametric table for part parameters
*
@@ -836,6 +928,7 @@ function loadPartTable(table, url, options={}) {
* query: extra query params for API request
* buttons: If provided, link buttons to selection status of this table
* disableFilters: If true, disable custom filters
+ * actions: Provide a callback function to construct an "actions" column
*/
// Ensure category detail is included
@@ -878,7 +971,7 @@ function loadPartTable(table, url, options={}) {
col = {
field: 'IPN',
- title: 'IPN',
+ title: '{% trans "IPN" %}',
};
if (!options.params.ordering) {
@@ -895,7 +988,7 @@ function loadPartTable(table, url, options={}) {
var name = row.full_name;
- var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/');
+ var display = imageHoverIcon(row.thumbnail) + renderLink(name, `/part/${row.pk}/`);
display += makePartIcons(row);
@@ -993,6 +1086,21 @@ function loadPartTable(table, url, options={}) {
}
});
+ // Push an "actions" column
+ if (options.actions) {
+ columns.push({
+ field: 'actions',
+ title: '',
+ switchable: false,
+ visible: true,
+ searchable: false,
+ sortable: false,
+ formatter: function(value, row) {
+ return options.actions(value, row);
+ }
+ });
+ }
+
var grid_view = options.gridView && inventreeLoad('part-grid-view') == 1;
$(table).inventreeTable({
@@ -1020,6 +1128,10 @@ function loadPartTable(table, url, options={}) {
$('#view-part-grid').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-part-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
}
+
+ if (options.onPostBody) {
+ options.onPostBody();
+ }
},
buttons: options.gridView ? [
{
diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js
index ba4238e6f7..5e92299f03 100644
--- a/InvenTree/templates/js/translated/stock.js
+++ b/InvenTree/templates/js/translated/stock.js
@@ -80,6 +80,20 @@ function serializeStockItem(pk, options={}) {
notes: {},
};
+ if (options.part) {
+ // Work out the next available serial number
+ inventreeGet(`/api/part/${options.part}/serial-numbers/`, {}, {
+ success: function(data) {
+ if (data.next) {
+ options.fields.serial_numbers.placeholder = `{% trans "Next available serial number" %}: ${data.next}`;
+ } else if (data.latest) {
+ options.fields.serial_numbers.placeholder = `{% trans "Latest serial number" %}: ${data.latest}`;
+ }
+ },
+ async: false,
+ });
+ }
+
constructForm(url, options);
}
@@ -144,10 +158,26 @@ function stockItemFields(options={}) {
// If a "trackable" part is selected, enable serial number field
if (data.trackable) {
enableFormInput('serial_numbers', opts);
- // showFormInput('serial_numbers', opts);
+
+ // Request part serial number information from the server
+ inventreeGet(`/api/part/${data.pk}/serial-numbers/`, {}, {
+ success: function(data) {
+ var placeholder = '';
+ if (data.next) {
+ placeholder = `{% trans "Next available serial number" %}: ${data.next}`;
+ } else if (data.latest) {
+ placeholder = `{% trans "Latest serial number" %}: ${data.latest}`;
+ }
+
+ setFormInputPlaceholder('serial_numbers', placeholder, opts);
+ }
+ });
+
} else {
clearFormInput('serial_numbers', opts);
disableFormInput('serial_numbers', opts);
+
+ setFormInputPlaceholder('serial_numbers', '{% trans "This part cannot be serialized" %}', opts);
}
// Enable / disable fields based on purchaseable status
@@ -1101,7 +1131,7 @@ function loadStockTable(table, options) {
col = {
field: 'part_detail.IPN',
- title: 'IPN',
+ title: '{% trans "IPN" %}',
sortName: 'part__IPN',
visible: params['part_detail'],
switchable: params['part_detail'],
diff --git a/InvenTree/templates/js/translated/table_filters.js b/InvenTree/templates/js/translated/table_filters.js
index 903774f8e5..409192f74d 100644
--- a/InvenTree/templates/js/translated/table_filters.js
+++ b/InvenTree/templates/js/translated/table_filters.js
@@ -74,6 +74,12 @@ function getAvailableTableFilters(tableKey) {
};
}
+ // Filters for the "related parts" table
+ if (tableKey == 'related') {
+ return {
+ };
+ }
+
// Filters for the "used in" table
if (tableKey == 'usedin') {
return {
diff --git a/docker/init.sh b/docker/init.sh
index b598a3ee79..7622806d0f 100644
--- a/docker/init.sh
+++ b/docker/init.sh
@@ -38,5 +38,8 @@ fi
cd ${INVENTREE_HOME}
+# Collect translation file stats
+invoke translate-stats
+
# Launch the CMD *after* the ENTRYPOINT completes
exec "$@"
diff --git a/tasks.py b/tasks.py
index b33af84384..21f7616d76 100644
--- a/tasks.py
+++ b/tasks.py
@@ -3,6 +3,8 @@
import os
import json
import sys
+import pathlib
+import re
try:
from invoke import ctask as task
@@ -469,6 +471,75 @@ def server(c, address="127.0.0.1:8000"):
manage(c, "runserver {address}".format(address=address), pty=True)
+@task(post=[translate_stats, static, server])
+def test_translations(c):
+ """
+ Add a fictional language to test if each component is ready for translations
+ """
+ import django
+ from django.conf import settings
+
+ # setup django
+ base_path = os.getcwd()
+ new_base_path = pathlib.Path('InvenTree').absolute()
+ sys.path.append(str(new_base_path))
+ os.chdir(new_base_path)
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'InvenTree.settings')
+ django.setup()
+
+ # Add language
+ print("Add dummy language...")
+ print("========================================")
+ manage(c, "makemessages -e py,html,js --no-wrap -l xx")
+
+ # change translation
+ print("Fill in dummy translations...")
+ print("========================================")
+
+ file_path = pathlib.Path(settings.LOCALE_PATHS[0], 'xx', 'LC_MESSAGES', 'django.po')
+ new_file_path = str(file_path) + '_new'
+
+ # complie regex
+ reg = re.compile(
+ r"[a-zA-Z0-9]{1}"+ # match any single letter and number
+ r"(?![^{\(\<]*[}\)\>])"+ # that is not inside curly brackets, brackets or a tag
+ r"(? replace regex matches with x in the read in (multi)string
+ file_new.write(f'msgstr "{reg.sub("x", last_string[7:-2])}"\n')
+ last_string = "" # reset (multi)string
+ elif line.startswith('msgid "'):
+ last_string = last_string + line # a new translatable string starts -> start append
+ file_new.write(line)
+ else:
+ if last_string:
+ last_string = last_string + line # a string is beeing read in -> continue appending
+ file_new.write(line)
+
+ # change out translation files
+ os.rename(file_path, str(file_path) + '_old')
+ os.rename(new_file_path, file_path)
+
+ # compile languages
+ print("Compile languages ...")
+ print("========================================")
+ manage(c, "compilemessages")
+
+ # reset cwd
+ os.chdir(base_path)
+
+ # set env flag
+ os.environ['TEST_TRANSLATIONS'] = 'True'
+
+
@task
def render_js_files(c):
"""