diff --git a/InvenTree/InvenTree/static/script/inventree/tables.js b/InvenTree/InvenTree/static/script/inventree/tables.js index 1a50780393..4d868f94b4 100644 --- a/InvenTree/InvenTree/static/script/inventree/tables.js +++ b/InvenTree/InvenTree/static/script/inventree/tables.js @@ -131,8 +131,7 @@ $.fn.inventreeTable = function(options) { // Callback when a column is changed options.onColumnSwitch = function(field, checked) { - console.log(`${field} -> ${checked}`); - + var columns = table.bootstrapTable('getVisibleColumns'); var text = visibleColumnString(columns); diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index b479575a35..3c848620d5 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -182,6 +182,10 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin): queryset = PartAttachment.objects.all() serializer_class = part_serializers.PartAttachmentSerializer + filter_backends = [ + DjangoFilterBackend, + ] + filter_fields = [ 'part', ] diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 6992d9bac4..47ef866e2e 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -9,7 +9,7 @@ {% if category %}

{{ category.name }} - {% if user.is_staff and roles.part.change %} + {% if user.is_staff and roles.part_category.change %} {% endif %}

@@ -20,18 +20,18 @@ {% endif %}

- {% if roles.part.add %} + {% if roles.part_category.add %} {% endif %} {% if category %} - {% if roles.part.change %} + {% if roles.part_category.change %} {% endif %} - {% if roles.part.delete %} + {% if roles.part_category.delete %} diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index f723193abb..59c33ea7b6 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -214,9 +214,9 @@ {% if part.active %} - + {% else %} - + {% endif %} {% trans "Active" %} diff --git a/InvenTree/part/templates/part/params.html b/InvenTree/part/templates/part/params.html index af1c4cdab6..d07a19f79f 100644 --- a/InvenTree/part/templates/part/params.html +++ b/InvenTree/part/templates/part/params.html @@ -37,7 +37,7 @@ {% if roles.part.change %} {% endif %} - {% if roles.part.delete %} + {% if roles.part.change %} {% endif %}
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 1d76860ac1..26cf93e56d 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -231,7 +231,7 @@ class PartAttachmentDelete(AjaxDeleteView): ajax_template_name = "attachment_delete.html" context_object_name = "attachment" - role_required = 'part.delete' + role_required = 'part.change' def get_data(self): return { @@ -1320,7 +1320,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): # Otherwise, check to see if there is a matching IPN try: if row['part_ipn']: - part_matches = [part for part in self.allowed_parts if row['part_ipn'].lower() == part.IPN.lower()] + part_matches = [part for part in self.allowed_parts if part.IPN and row['part_ipn'].lower() == str(part.IPN.lower())] # Check for single match if len(part_matches) == 1: @@ -2073,7 +2073,7 @@ class PartParameterEdit(AjaxUpdateView): class PartParameterDelete(AjaxDeleteView): """ View for deleting a PartParameter """ - role_required = 'part.delete' + role_required = 'part.change' model = PartParameter ajax_template_name = 'part/param_delete.html' @@ -2088,7 +2088,7 @@ class CategoryDetail(InvenTreeRoleMixin, DetailView): queryset = PartCategory.objects.all().prefetch_related('children') template_name = 'part/category_partlist.html' - role_required = 'part.view' + role_required = ['part_category.view', 'part.view'] def get_context_data(self, **kwargs): @@ -2138,7 +2138,7 @@ class CategoryEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = _('Edit Part Category') - role_required = 'part.change' + role_required = 'part_category.change' def get_context_data(self, **kwargs): context = super(CategoryEdit, self).get_context_data(**kwargs).copy() @@ -2177,7 +2177,7 @@ class CategoryDelete(AjaxDeleteView): context_object_name = 'category' success_url = '/part/' - role_required = 'part.delete' + role_required = 'part_category.delete' def get_data(self): return { @@ -2193,7 +2193,7 @@ class CategoryCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' form_class = part_forms.EditCategoryForm - role_required = 'part.add' + role_required = 'part_category.add' def get_context_data(self, **kwargs): """ Add extra context data to template. @@ -2233,7 +2233,7 @@ class CategoryCreate(AjaxCreateView): class CategoryParameterTemplateCreate(AjaxCreateView): """ View for creating a new PartCategoryParameterTemplate """ - role_required = 'part.add' + role_required = 'part_category.change' model = PartCategoryParameterTemplate form_class = part_forms.EditCategoryParameterTemplateForm @@ -2336,7 +2336,7 @@ class CategoryParameterTemplateCreate(AjaxCreateView): class CategoryParameterTemplateEdit(AjaxUpdateView): """ View for editing a PartCategoryParameterTemplate """ - role_required = 'part.change' + role_required = 'part_category.change' model = PartCategoryParameterTemplate form_class = part_forms.EditCategoryParameterTemplateForm @@ -2395,7 +2395,7 @@ class CategoryParameterTemplateEdit(AjaxUpdateView): class CategoryParameterTemplateDelete(AjaxDeleteView): """ View for deleting an existing PartCategoryParameterTemplate """ - role_required = 'part.delete' + role_required = 'part_category.change' model = PartCategoryParameterTemplate ajax_form_title = _("Delete Category Parameter Template") @@ -2554,7 +2554,7 @@ class BomItemDelete(AjaxDeleteView): context_object_name = 'item' ajax_form_title = _('Confim BOM item deletion') - role_required = 'part.delete' + role_required = 'part.change' class PartSalePriceBreakCreate(AjaxCreateView): diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 70ad1abc18..6048d5e248 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -208,6 +208,7 @@ class StockItemSerializer(InvenTreeModelSerializer): 'stale', 'status', 'status_text', + 'stocktake_date', 'supplier_part', 'supplier_part_detail', 'tracking_items', diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index fef3428373..765a4bd903 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -8,7 +8,7 @@ {% if location %}

{{ location.name }} - {% if user.is_staff and roles.stock.change %} + {% if user.is_staff and roles.stock_location.change %} {% endif %}

@@ -18,7 +18,7 @@

{% trans "All stock items" %}

{% endif %}
- {% 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',