- {% if roles.stock.add %}
+ {% if roles.stock_location.add %}
@@ -41,11 +41,13 @@
{% trans "Count stock" %}
+ {% endif %}
+ {% if roles.stock_location.change %}
diff --git a/InvenTree/stock/templates/stock/location_list.html b/InvenTree/stock/templates/stock/location_list.html
index 4ad30e1310..a2ea4a361a 100644
--- a/InvenTree/stock/templates/stock/location_list.html
+++ b/InvenTree/stock/templates/stock/location_list.html
@@ -1,6 +1,6 @@
{% extends "collapse.html" %}
-{% if roles.stock.view %}
+{% if roles.stock_location.view or roles.stock.view %}
{% block collapse_title %}
Sub-Locations
{{ children|length }}
{% endblock %}
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index ab6f64fb44..5d505b5280 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -73,7 +73,7 @@ class StockLocationDetail(InvenTreeRoleMixin, DetailView):
template_name = 'stock/location.html'
queryset = StockLocation.objects.all()
model = StockLocation
- role_required = 'stock.view'
+ role_required = ['stock_location.view', 'stock.view']
class StockItemDetail(InvenTreeRoleMixin, DetailView):
@@ -121,7 +121,7 @@ class StockLocationEdit(AjaxUpdateView):
context_object_name = 'location'
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Stock Location')
- role_required = 'stock.change'
+ role_required = 'stock_location.change'
def get_form(self):
""" Customize form data for StockLocation editing.
@@ -146,7 +146,7 @@ class StockLocationQRCode(QRCodeView):
""" View for displaying a QR code for a StockLocation object """
ajax_form_title = _("Stock Location QR code")
- role_required = 'stock.view'
+ role_required = ['stock_location.view', 'stock.view']
def get_qr_data(self):
""" Generate QR code data for the StockLocation """
@@ -1361,7 +1361,7 @@ class StockLocationCreate(AjaxCreateView):
context_object_name = 'location'
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Create new Stock Location')
- role_required = 'stock.add'
+ role_required = 'stock_location.add'
def get_initial(self):
initials = super(StockLocationCreate, self).get_initial().copy()
@@ -1721,7 +1721,7 @@ class StockItemCreate(AjaxCreateView):
item = form.save(commit=False)
item.user = self.request.user
- item.save()
+ item.save(user=self.request.user)
return item
@@ -1732,7 +1732,7 @@ class StockItemCreate(AjaxCreateView):
item = form.save(commit=False)
item.user = self.request.user
- item.save()
+ item.save(user=self.request.user)
return item
@@ -1748,7 +1748,7 @@ class StockLocationDelete(AjaxDeleteView):
ajax_template_name = 'stock/location_delete.html'
context_object_name = 'location'
ajax_form_title = _('Delete Stock Location')
- role_required = 'stock.delete'
+ role_required = 'stock_location.delete'
class StockItemDelete(AjaxDeleteView):
diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js
index 299045cfa5..e1b3b46cc0 100644
--- a/InvenTree/templates/js/bom.js
+++ b/InvenTree/templates/js/bom.js
@@ -255,6 +255,38 @@ function loadBomTable(table, options) {
});
*/
}
+
+ cols.push(
+ {
+ 'field': 'can_build',
+ 'title': '{% trans "Can Build" %}',
+ formatter: function(value, row, index, field) {
+ var can_build = 0;
+
+ if (row.quantity > 0) {
+ can_build = row.sub_part_detail.stock / row.quantity;
+ }
+
+ return +can_build.toFixed(2);
+ },
+ sorter: function(valA, valB, rowA, rowB) {
+ // Function to sort the "can build" quantity
+ var cb_a = 0;
+ var cb_b = 0;
+
+ if (rowA.quantity > 0) {
+ cb_a = rowA.sub_part_detail.stock / rowA.quantity;
+ }
+
+ if (rowB.quantity > 0) {
+ cb_b = rowB.sub_part_detail.stock / rowB.quantity;
+ }
+
+ return (cb_a > cb_b) ? 1 : -1;
+ },
+ sortable: true,
+ }
+ )
// Part notes
cols.push(
diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js
index e3f124d252..e9cb5e2696 100644
--- a/InvenTree/templates/js/stock.js
+++ b/InvenTree/templates/js/stock.js
@@ -6,8 +6,18 @@
* Requires api.js to be loaded first
*/
-/* Functions for interacting with stock management forms
- */
+
+function stockStatusCodes() {
+ return [
+ {% for code in StockStatus.list %}
+ {
+ key: {{ code.key }},
+ text: "{{ code.value }}",
+ },
+ {% endfor %}
+ ];
+}
+
function removeStockRow(e) {
// Remove a selected row from a stock modal form
@@ -590,6 +600,11 @@ function loadStockTable(table, options) {
return locationDetail(row);
}
},
+ {
+ field: 'stocktake_date',
+ title: '{% trans "Stocktake" %}',
+ sortable: true,
+ },
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
{
@@ -677,6 +692,93 @@ function loadStockTable(table, options) {
});
});
+ $("#multi-item-set-status").click(function() {
+ // Select and set the STATUS field for selected stock items
+ var selections = $("#stock-table").bootstrapTable('getSelections');
+
+ // Select stock status
+ var modal = '#modal-form';
+
+ var status_list = makeOptionsList(
+ stockStatusCodes(),
+ function(item) {
+ return item.text;
+ },
+ function (item) {
+ return item.key;
+ }
+ );
+
+ // Add an empty option at the start of the list
+ status_list.unshift('
');
+
+ // Construct form
+ var html = `
+
`;
+
+ openModal({
+ modal: modal,
+ });
+
+ modalEnable(modal, true);
+ modalSetTitle(modal, '{% trans "Set Stock Status" %}');
+ modalSetContent(modal, html);
+
+ attachSelect(modal);
+
+ modalSubmit(modal, function() {
+ var label = $(modal).find('#id_status');
+
+ var status_code = label.val();
+
+ closeModal(modal);
+
+ if (!status_code) {
+ showAlertDialog(
+ '{% trans "Select Status Code" %}',
+ '{% trans "Status code must be selected" %}'
+ );
+
+ return;
+ }
+
+ var requests = [];
+
+ selections.forEach(function(item) {
+ var url = `/api/stock/${item.pk}/`;
+
+ requests.push(
+ inventreePut(
+ url,
+ {
+ status: status_code,
+ },
+ {
+ method: 'PATCH',
+ success: function() {
+ }
+ }
+ )
+ );
+ });
+
+ $.when.apply($, requests).then(function() {
+ $("#stock-table").bootstrapTable('refresh');
+ });
+ })
+ });
+
$("#multi-item-delete").click(function() {
var selections = $("#stock-table").bootstrapTable("getSelections");
diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html
index 51f7c277db..f39f9c733a 100644
--- a/InvenTree/templates/stock_table.html
+++ b/InvenTree/templates/stock_table.html
@@ -23,6 +23,7 @@
{% trans "Count stock" %}
{% trans "Move stock" %}
{% trans "Order stock" %}
+
{% trans "Change stock status" %}
{% endif %}
{% if roles.stock.delete %}
{% trans "Delete Stock" %}
diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py
index 29496d02a7..c84f1310ce 100644
--- a/InvenTree/users/admin.py
+++ b/InvenTree/users/admin.py
@@ -30,6 +30,8 @@ class RuleSetInline(admin.TabularInline):
max_num = len(RuleSet.RULESET_CHOICES)
min_num = 1
extra = 0
+ # TODO: find better way to order inlines
+ ordering = ['name']
class InvenTreeGroupAdminForm(forms.ModelForm):
@@ -87,7 +89,8 @@ class RoleGroupAdmin(admin.ModelAdmin):
RuleSetInline,
]
- list_display = ('name', 'admin', 'part', 'stock', 'build', 'purchase_order', 'sales_order')
+ list_display = ('name', 'admin', 'part_category', 'part', 'stock_location',
+ 'stock_item', 'build', 'purchase_order', 'sales_order')
def get_rule_set(self, obj, rule_set_type):
''' Return list of permissions for the given ruleset '''
@@ -130,10 +133,16 @@ class RoleGroupAdmin(admin.ModelAdmin):
def admin(self, obj):
return self.get_rule_set(obj, 'admin')
+ def part_category(self, obj):
+ return self.get_rule_set(obj, 'part_category')
+
def part(self, obj):
return self.get_rule_set(obj, 'part')
- def stock(self, obj):
+ def stock_location(self, obj):
+ return self.get_rule_set(obj, 'stock_location')
+
+ def stock_item(self, obj):
return self.get_rule_set(obj, 'stock')
def build(self, obj):
diff --git a/InvenTree/users/migrations/0004_auto_20210113_1909.py b/InvenTree/users/migrations/0004_auto_20210113_1909.py
new file mode 100644
index 0000000000..d762dc4c08
--- /dev/null
+++ b/InvenTree/users/migrations/0004_auto_20210113_1909.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2021-01-13 19:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0003_auto_20201005_2227'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='ruleset',
+ name='name',
+ field=models.CharField(choices=[('admin', 'Admin'), ('part_category', 'Part Categories'), ('part', 'Parts'), ('stock_location', 'Stock Locations'), ('stock', 'Stock Items'), ('build', 'Build Orders'), ('purchase_order', 'Purchase Orders'), ('sales_order', 'Sales Orders')], help_text='Permission set', max_length=50),
+ ),
+ ]
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index b54cddf7c4..776514fc9f 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -25,8 +25,10 @@ class RuleSet(models.Model):
RULESET_CHOICES = [
('admin', _('Admin')),
+ ('part_category', _('Part Categories')),
('part', _('Parts')),
- ('stock', _('Stock')),
+ ('stock_location', _('Stock Locations')),
+ ('stock', _('Stock Items')),
('build', _('Build Orders')),
('purchase_order', _('Purchase Orders')),
('sales_order', _('Sales Orders')),
@@ -48,21 +50,25 @@ class RuleSet(models.Model):
'authtoken_token',
'users_ruleset',
],
+ 'part_category': [
+ 'part_partcategory',
+ 'part_partcategoryparametertemplate',
+ ],
'part': [
'part_part',
'part_bomitem',
- 'part_partcategory',
'part_partattachment',
'part_partsellpricebreak',
'part_parttesttemplate',
'part_partparametertemplate',
'part_partparameter',
'part_partrelated',
- 'part_partcategoryparametertemplate',
+ ],
+ 'stock_location': [
+ 'stock_stocklocation',
],
'stock': [
'stock_stockitem',
- 'stock_stocklocation',
'stock_stockitemattachment',
'stock_stockitemtracking',
'stock_stockitemtestresult',