diff --git a/InvenTree/InvenTree/static/script/inventree/modals.js b/InvenTree/InvenTree/static/script/inventree/modals.js
index a17cee0d2c..1cb5346a68 100644
--- a/InvenTree/InvenTree/static/script/inventree/modals.js
+++ b/InvenTree/InvenTree/static/script/inventree/modals.js
@@ -469,6 +469,9 @@ function openModal(options) {
$(modal).on('shown.bs.modal', function() {
$(modal + ' .modal-form-content').scrollTop(0);
+ if (options.focus) {
+ getFieldByName(modal, options.focus).focus();
+ }
});
// Prevent 'enter' key from submitting the form using the normal method
@@ -745,6 +748,7 @@ function launchModalForm(url, options = {}) {
* after_render - Callback function to run after form is rendered
* secondary - List of secondary modals to attach
* callback - List of callback functions to attach to inputs
+ * focus - Select which field to focus on by default
*/
var modal = options.modal || '#modal-form';
@@ -763,6 +767,7 @@ function launchModalForm(url, options = {}) {
modal: modal,
submit_text: submit_text,
close_text: close_text,
+ focus: options.focus
});
},
success: function(response) {
diff --git a/InvenTree/InvenTree/static/script/inventree/tables.js b/InvenTree/InvenTree/static/script/inventree/tables.js
index e319aa8440..be0e1e6325 100644
--- a/InvenTree/InvenTree/static/script/inventree/tables.js
+++ b/InvenTree/InvenTree/static/script/inventree/tables.js
@@ -80,11 +80,28 @@ function reloadTable(table, filters) {
}
+function visibleColumnString(columns) {
+ /* Generate a list of "visible" columns to save to file. */
+
+ var fields = [];
+
+ columns.forEach(function(column) {
+ if (column.switchable && column.visible) {
+ fields.push(column.field);
+ }
+ });
+
+ return fields.join(',');
+}
+
+
/* Wrapper function for bootstrapTable.
* Sets some useful defaults, and manage persistent settings.
*/
$.fn.inventreeTable = function(options) {
+ var table = this;
+
var tableName = options.name || 'table';
var varName = tableName + '-pagesize';
@@ -95,14 +112,51 @@ $.fn.inventreeTable = function(options) {
options.rememberOrder = true;
options.sortable = true;
options.search = true;
+ options.showColumns = true;
// Callback to save pagination data
options.onPageChange = function(number, size) {
inventreeSave(varName, size);
};
+ // Callback when a column is changed
+ options.onColumnSwitch = function(field, checked) {
+ console.log(`${field} -> ${checked}`);
+
+ var columns = table.bootstrapTable('getVisibleColumns');
+
+ var text = visibleColumnString(columns);
+
+ // Save visible columns
+ inventreeSave(`table_columns_${tableName}`, text);
+ };
+
// Standard options for all tables
- this.bootstrapTable(options);
+ table.bootstrapTable(options);
+
+ // Load visible column list from memory
+ // Load visible column list
+ var visibleColumns = inventreeLoad(`table_columns_${tableName}`, null);
+
+ // If a set of visible columns has been saved, load!
+ if (visibleColumns) {
+ var columns = visibleColumns.split(",");
+
+ // Which columns are currently visible?
+ var visible = table.bootstrapTable('getVisibleColumns');
+
+ if (visible) {
+ visible.forEach(function(column) {
+
+ // Visible field should *not* be visible! (hide it!)
+ if (column.switchable && !columns.includes(column.field)) {
+ table.bootstrapTable('hideColumn', column.field);
+ }
+ });
+ } else {
+ console.log('Could not get list of visible columns!');
+ }
+ }
}
function customGroupSorter(sortName, sortOrder, sortData) {
diff --git a/InvenTree/order/migrations/0036_auto_20200831_0912.py b/InvenTree/order/migrations/0036_auto_20200831_0912.py
new file mode 100644
index 0000000000..1329d0a8d5
--- /dev/null
+++ b/InvenTree/order/migrations/0036_auto_20200831_0912.py
@@ -0,0 +1,35 @@
+# Generated by Django 3.0.7 on 2020-08-31 09:12
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('company', '0023_auto_20200808_0715'),
+ ('order', '0035_auto_20200513_0016'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='purchaseorder',
+ name='complete_date',
+ field=models.DateField(blank=True, help_text='Date order was completed', null=True),
+ ),
+ migrations.AlterField(
+ model_name='purchaseorder',
+ name='issue_date',
+ field=models.DateField(blank=True, help_text='Date order was issued', null=True),
+ ),
+ migrations.AlterField(
+ model_name='purchaseorder',
+ name='supplier',
+ field=models.ForeignKey(help_text='Company from which the items are being ordered', limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='purchase_orders', to='company.Company'),
+ ),
+ migrations.AlterField(
+ model_name='salesorder',
+ name='customer',
+ field=models.ForeignKey(help_text='Company to which the items are being sold', limit_choices_to={'is_customer': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales_orders', to='company.Company'),
+ ),
+ ]
diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py
index b607d314a0..7d561b95ba 100644
--- a/InvenTree/order/models.py
+++ b/InvenTree/order/models.py
@@ -137,7 +137,7 @@ class PurchaseOrder(Order):
return "PO {ref} - {company}".format(ref=self.reference, company=self.supplier.name)
status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(),
- help_text='Purchase order status')
+ help_text=_('Purchase order status'))
supplier = models.ForeignKey(
Company, on_delete=models.CASCADE,
@@ -145,7 +145,7 @@ class PurchaseOrder(Order):
'is_supplier': True,
},
related_name='purchase_orders',
- help_text=_('Supplier')
+ help_text=_('Company from which the items are being ordered')
)
supplier_reference = models.CharField(max_length=64, blank=True, help_text=_("Supplier order reference code"))
@@ -157,9 +157,9 @@ class PurchaseOrder(Order):
related_name='+'
)
- issue_date = models.DateField(blank=True, null=True)
+ issue_date = models.DateField(blank=True, null=True, help_text=_('Date order was issued'))
- complete_date = models.DateField(blank=True, null=True)
+ complete_date = models.DateField(blank=True, null=True, help_text=_('Date order was completed'))
def get_absolute_url(self):
return reverse('po-detail', kwargs={'pk': self.id})
@@ -311,11 +311,11 @@ class SalesOrder(Order):
null=True,
limit_choices_to={'is_customer': True},
related_name='sales_orders',
- help_text=_("Customer"),
+ help_text=_("Company to which the items are being sold"),
)
status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(),
- help_text='Purchase order status')
+ help_text=_('Purchase order status'))
customer_reference = models.CharField(max_length=64, blank=True, help_text=_("Customer order reference code"))
diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html
index de066500b2..9b9eddb887 100644
--- a/InvenTree/order/templates/order/purchase_order_detail.html
+++ b/InvenTree/order/templates/order/purchase_order_detail.html
@@ -127,6 +127,7 @@ function setupCallbacks() {
$("#po-table").inventreeTable({
onPostBody: setupCallbacks,
+ name: 'purchaseorder',
formatNoMatches: function() { return "{% trans 'No line items found' %}"; },
queryParams: {
order: {{ order.id }},
@@ -138,6 +139,7 @@ $("#po-table").inventreeTable({
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
field: 'part',
diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html
index 75c5fc3d7b..b6cc761cc7 100644
--- a/InvenTree/order/templates/order/sales_order_detail.html
+++ b/InvenTree/order/templates/order/sales_order_detail.html
@@ -190,6 +190,7 @@ $("#so-lines-table").inventreeTable({
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
sortable: true,
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index e5b035f856..26dd04ac52 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -1080,12 +1080,21 @@ class Part(MPTTModel):
"""
- n = self.attachments.count()
+ return self.part_attachments.count()
- if self.variant_of:
- n += self.variant_of.attachments.count()
+ @property
+ def part_attachments(self):
+ """
+ Return *all* attachments for this part,
+ potentially including attachments for template parts
+ above this one.
+ """
- return n
+ ancestors = self.get_ancestors(include_self=True)
+
+ attachments = PartAttachment.objects.filter(part__in=ancestors)
+
+ return attachments
def sales_orders(self):
""" Return a list of sales orders which reference this part """
diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html
index 049ef0cd7a..aa31cd3f32 100644
--- a/InvenTree/part/templates/part/attachments.html
+++ b/InvenTree/part/templates/part/attachments.html
@@ -9,7 +9,7 @@
-{% include "attachment_table.html" with attachments=part.attachments.all %}
+{% include "attachment_table.html" with attachments=part.part_attachments %}
{% endblock %}
diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html
index ce0491a165..3a6605847e 100644
--- a/InvenTree/part/templates/part/used_in.html
+++ b/InvenTree/part/templates/part/used_in.html
@@ -29,6 +29,7 @@
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
field: 'part_detail',
diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py
index bba8e50d3d..a5264f7b2b 100644
--- a/InvenTree/report/models.py
+++ b/InvenTree/report/models.py
@@ -127,7 +127,7 @@ class ReportTemplateBase(models.Model):
except TexError:
return TexResponse(rendered, filename="error.tex")
else:
- return ValidationError("Enable LaTeX support in config.yaml")
+ raise ValidationError("Enable LaTeX support in config.yaml")
elif self.extension in ['.htm', '.html']:
# Render HTML template to PDF
wp = WeasyprintReportMixin(request, self.template_name, **kwargs)
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 4368f654cf..7741c67129 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -186,7 +186,7 @@ class StockCount(StockAdjust):
class StockAdd(StockAdjust):
"""
- Endpoint for adding stock
+ Endpoint for adding a quantity of stock to an existing StockItem
"""
def post(self, request, *args, **kwargs):
@@ -204,7 +204,7 @@ class StockAdd(StockAdjust):
class StockRemove(StockAdjust):
"""
- Endpoint for removing stock.
+ Endpoint for removing a quantity of stock from an existing StockItem.
"""
def post(self, request, *args, **kwargs):
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index bed3f8f7c1..cccc138523 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -99,26 +99,10 @@ class StockItemSerializer(InvenTreeModelSerializer):
return queryset
- belongs_to = serializers.PrimaryKeyRelatedField(read_only=True)
-
- build_order = serializers.PrimaryKeyRelatedField(read_only=True)
-
- customer = serializers.PrimaryKeyRelatedField(read_only=True)
-
- location = serializers.PrimaryKeyRelatedField(read_only=True)
-
- in_stock = serializers.BooleanField(read_only=True)
-
- sales_order = serializers.PrimaryKeyRelatedField(read_only=True)
-
status_text = serializers.CharField(source='get_status_display', read_only=True)
-
- supplier_part = serializers.PrimaryKeyRelatedField(read_only=True)
-
+
supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True)
- part = serializers.PrimaryKeyRelatedField(read_only=True)
-
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
location_detail = LocationBriefSerializer(source='location', many=False, read_only=True)
diff --git a/InvenTree/stock/templates/stock/item_tests.html b/InvenTree/stock/templates/stock/item_tests.html
index 10bb0950e4..6bc120711d 100644
--- a/InvenTree/stock/templates/stock/item_tests.html
+++ b/InvenTree/stock/templates/stock/item_tests.html
@@ -73,6 +73,7 @@ $("#add-test-result").click(function() {
stock_item: {{ item.id }},
},
success: reloadTable,
+ focus: 'test',
}
);
});
@@ -89,6 +90,7 @@ $("#test-result-table").on('click', '.button-test-add', function() {
test: test_name
},
success: reloadTable,
+ focus: 'value',
}
);
});
diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py
index cd598e8538..47f2e5fcb1 100644
--- a/InvenTree/stock/test_api.py
+++ b/InvenTree/stock/test_api.py
@@ -81,6 +81,59 @@ class StockItemTest(StockAPITestCase):
response = self.client.get(self.list_url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
+ def test_stock_item_create(self):
+ """
+ Test creation of a StockItem via the API
+ """
+
+ # POST with an empty part reference
+
+ response = self.client.post(
+ self.list_url,
+ data={
+ 'quantity': 10,
+ 'location': 1
+ }
+ )
+
+ self.assertContains(response, 'This field is required', status_code=status.HTTP_400_BAD_REQUEST)
+
+ # POST with an invalid part reference
+
+ response = self.client.post(
+ self.list_url,
+ data={
+ 'quantity': 10,
+ 'location': 1,
+ 'part': 10000000,
+ }
+ )
+
+ self.assertContains(response, 'does not exist', status_code=status.HTTP_400_BAD_REQUEST)
+
+ # POST without quantity
+ response = self.client.post(
+ self.list_url,
+ data={
+ 'part': 1,
+ 'location': 1,
+ }
+ )
+
+ self.assertContains(response, 'This field is required', status_code=status.HTTP_400_BAD_REQUEST)
+
+ # POST with quantity and part and location
+ response = self.client.post(
+ self.list_url,
+ data={
+ 'part': 1,
+ 'location': 1,
+ 'quantity': 10,
+ }
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
class StocktakeTest(StockAPITestCase):
"""
diff --git a/InvenTree/templates/InvenTree/settings/currency.html b/InvenTree/templates/InvenTree/settings/currency.html
index 9c602a67fc..c585011ecd 100644
--- a/InvenTree/templates/InvenTree/settings/currency.html
+++ b/InvenTree/templates/InvenTree/settings/currency.html
@@ -37,6 +37,7 @@
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
field: 'symbol',
diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html
index 49631cd67d..5eba81a72a 100644
--- a/InvenTree/templates/InvenTree/settings/part.html
+++ b/InvenTree/templates/InvenTree/settings/part.html
@@ -31,6 +31,7 @@
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
field: 'name',
diff --git a/InvenTree/templates/js/bom.html b/InvenTree/templates/js/bom.html
index 122b1b7ba8..cab2a08b13 100644
--- a/InvenTree/templates/js/bom.html
+++ b/InvenTree/templates/js/bom.html
@@ -111,6 +111,7 @@ function loadBomTable(table, options) {
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
];
@@ -320,6 +321,7 @@ function loadBomTable(table, options) {
parentIdField: 'parentId',
treeShowField: 'sub_part',
showColumns: true,
+ name: 'bom',
sortable: true,
search: true,
rowStyle: function(row, index) {
diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.html
index b48e6a2a09..8682e9bd81 100644
--- a/InvenTree/templates/js/build.html
+++ b/InvenTree/templates/js/build.html
@@ -21,12 +21,14 @@ function loadBuildTable(table, options) {
url: options.url,
queryParams: filters,
groupBy: false,
+ name: 'builds',
original: params,
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
field: 'title',
diff --git a/InvenTree/templates/js/company.html b/InvenTree/templates/js/company.html
index 8b278e2a21..ef80070d99 100644
--- a/InvenTree/templates/js/company.html
+++ b/InvenTree/templates/js/company.html
@@ -27,16 +27,20 @@ function loadCompanyTable(table, url, options={}) {
queryParams: filters,
groupBy: false,
formatNoMatches: function() { return "{% trans "No company information found" %}"; },
+ showColumns: true,
+ name: 'company',
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
field: 'name',
title: '{% trans "Company" %}',
sortable: true,
+ switchable: false,
formatter: function(value, row, index, field) {
var html = imageHoverIcon(row.image) + renderLink(value, row.url);
@@ -97,11 +101,13 @@ function loadSupplierPartTable(table, url, options) {
url: url,
method: 'get',
queryParams: filters,
+ name: 'supplierparts',
groupBy: false,
formatNoMatches: function() { return "{% trans "No supplier parts found" %}"; },
columns: [
{
checkbox: true,
+ switchable: false,
},
{
sortable: true,
diff --git a/InvenTree/templates/js/order.html b/InvenTree/templates/js/order.html
index 17bd67cabb..c16b40583c 100644
--- a/InvenTree/templates/js/order.html
+++ b/InvenTree/templates/js/order.html
@@ -121,6 +121,7 @@ function loadPurchaseOrderTable(table, options) {
$(table).inventreeTable({
url: options.url,
queryParams: filters,
+ name: 'purchaseorder',
groupBy: false,
original: options.params,
formatNoMatches: function() { return "{% trans "No purchase orders found" %}"; },
@@ -129,6 +130,7 @@ function loadPurchaseOrderTable(table, options) {
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
sortable: true,
@@ -194,6 +196,7 @@ function loadSalesOrderTable(table, options) {
$(table).inventreeTable({
url: options.url,
queryParams: filters,
+ name: 'salesorder',
groupBy: false,
original: options.params,
formatNoMatches: function() { return "{% trans "No sales orders found" %}"; },
@@ -202,6 +205,7 @@ function loadSalesOrderTable(table, options) {
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
sortable: true,
diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html
index c9901cdb68..a1c3283c1c 100644
--- a/InvenTree/templates/js/part.html
+++ b/InvenTree/templates/js/part.html
@@ -237,6 +237,7 @@ function loadPartTable(table, url, options={}) {
method: 'get',
queryParams: filters,
groupBy: false,
+ name: 'part',
original: params,
formatNoMatches: function() { return "{% trans "No parts found" %}"; },
columns: columns,
@@ -338,6 +339,7 @@ function loadPartTestTemplateTable(table, options) {
},
url: "{% url 'api-part-test-template-list' %}",
queryParams: filters,
+ name: 'testtemplate',
original: original,
columns: [
{
diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html
index 9393b29739..bb0673c16d 100644
--- a/InvenTree/templates/js/stock.html
+++ b/InvenTree/templates/js/stock.html
@@ -73,6 +73,7 @@ function loadStockTestResultsTable(table, options) {
table.inventreeTable({
url: "{% url 'api-part-test-template-list' %}",
method: 'get',
+ name: 'testresult',
formatNoMatches: function() {
return "{% trans 'No test results found' %}";
},
@@ -84,6 +85,7 @@ function loadStockTestResultsTable(table, options) {
field: 'pk',
title: 'ID',
visible: false,
+ switchable: false,
},
{
field: 'test_name',
@@ -264,6 +266,7 @@ function loadStockTable(table, options) {
queryParams: filters,
customSort: customGroupSorter,
groupBy: true,
+ name: 'stock',
original: original,
showColumns: true,
groupByField: options.groupByField || 'part',
@@ -405,6 +408,7 @@ function loadStockTable(table, options) {
checkbox: true,
title: '{% trans "Select" %}',
searchable: false,
+ switchable: false,
},
{
field: 'pk',
diff --git a/requirements.txt b/requirements.txt
index 3687341166..38b5a37b6d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -23,4 +23,6 @@ rapidfuzz==0.7.6 # Fuzzy string matching
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
\ No newline at end of file
+django-debug-toolbar==2.2 # Debug / profiling toolbar
+
+inventree # Install the latest version of the InvenTree API python library
\ No newline at end of file
diff --git a/tasks.py b/tasks.py
index 3d92cec5c4..b7bda95033 100644
--- a/tasks.py
+++ b/tasks.py
@@ -47,7 +47,7 @@ def managePyPath():
return os.path.join(managePyDir(), 'manage.py')
-def manage(c, cmd):
+def manage(c, cmd, pty=False):
"""
Runs a given command against django's "manage.py" script.
@@ -59,7 +59,7 @@ def manage(c, cmd):
c.run('cd {path} && python3 manage.py {cmd}'.format(
path=managePyDir(),
cmd=cmd
- ))
+ ), pty=pty)
@task(help={'length': 'Length of secret key (default=50)'})
def key(c, length=50, force=False):
@@ -106,7 +106,7 @@ def superuser(c):
Create a superuser (admin) account for the database.
"""
- manage(c, 'createsuperuser')
+ manage(c, 'createsuperuser', pty=True)
@task
def migrate(c):