diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index 669b55b0c0..3e1f98ffc2 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -6,6 +6,7 @@ Provides extra global data to all templates. from InvenTree.status_codes import SalesOrderStatus, PurchaseOrderStatus from InvenTree.status_codes import BuildStatus, StockStatus +from InvenTree.status_codes import StockHistoryCode import InvenTree.status @@ -65,6 +66,7 @@ def status_codes(request): 'PurchaseOrderStatus': PurchaseOrderStatus, 'BuildStatus': BuildStatus, 'StockStatus': StockStatus, + 'StockHistoryCode': StockHistoryCode, } diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 53b747a9ad..63fc8a491c 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -7,6 +7,8 @@ class StatusCode: This is used to map a set of integer values to text. """ + colors = {} + @classmethod def render(cls, key, large=False): """ diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 3443c9982f..b0b6ae1c24 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -21,7 +21,7 @@ from .models import StockItemTestResult from part.models import Part, PartCategory from part.serializers import PartBriefSerializer -from company.models import SupplierPart +from company.models import Company, SupplierPart from company.serializers import CompanySerializer, SupplierPartSerializer from order.models import PurchaseOrder @@ -100,6 +100,16 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): return self.serializer_class(*args, **kwargs) + def update(self, request, *args, **kwargs): + """ + Record the user who updated the item + """ + + # TODO: Record the user! + # user = request.user + + return super().update(request, *args, **kwargs) + class StockFilter(FilterSet): """ FilterSet for advanced stock filtering. @@ -374,25 +384,25 @@ class StockList(generics.ListCreateAPIView): we can pre-fill the location automatically. """ + user = request.user + serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - item = serializer.save() + item = serializer.save(user=user, commit=False) # A location was *not* specified - try to infer it if 'location' not in request.data: - location = item.part.get_default_location() - - if location is not None: - item.location = location - item.save() + item.location = item.part.get_default_location() # An expiry date was *not* specified - try to infer it! if 'expiry_date' not in request.data: if item.part.default_expiry > 0: item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry) - item.save() + + # Finally, save the item + item.save(user=user) # Return a response headers = self.get_success_headers(serializer.data) @@ -1029,7 +1039,7 @@ class StockTrackingList(generics.ListAPIView): if 'customer' in deltas: try: customer = Company.objects.get(pk=deltas['customer']) - serializer = CompanySerializer(location) + serializer = CompanySerializer(customer) deltas['customer_detail'] = serializer.data except: pass diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 3fb72ebe1e..6d7f8d40ed 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -393,6 +393,18 @@ class AdjustStockForm(forms.ModelForm): ] +class EditStockItemStatusForm(HelperForm): + """ + Simple form for editing StockItem status field + """ + + class Meta: + model = StockItem + fields = [ + 'status', + ] + + class EditStockItemForm(HelperForm): """ Form for editing a StockItem object. Note that not all fields can be edited here (even if they can be specified during creation. diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4076b27c97..bae1bc5135 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -183,20 +183,46 @@ class StockItem(MPTTModel): self.validate_unique() self.clean() + user = kwargs.pop('user', None) + # If 'add_note = False' specified, then no tracking note will be added for item creation add_note = kwargs.pop('add_note', True) + notes = kwargs.pop('notes', '') + if not self.pk: # StockItem has not yet been saved add_note = add_note and True else: # StockItem has already been saved + + # Check if "interesting" fields have been changed + # (we wish to record these as historical records) + + try: + old = StockItem.objects.get(pk=self.pk) + + deltas = {} + + # Status changed? + if not old.status == self.status: + deltas['status'] = self.status + + # TODO - Other interesting changes we are interested in... + + if add_note and len(deltas) > 0: + self.add_tracking_entry( + StockHistoryCode.EDITED, + user, + deltas=deltas, + notes=notes, + ) + + except (ValueError, StockItem.DoesNotExist): + pass + add_note = False - user = kwargs.pop('user', None) - - add_note = add_note and kwargs.pop('note', True) - super(StockItem, self).save(*args, **kwargs) if add_note: @@ -209,6 +235,7 @@ class StockItem(MPTTModel): StockHistoryCode.CREATED, user, deltas=tracking_info, + notes=notes, location=self.location, quantity=float(self.quantity), ) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index c7e0dc15dd..54d6b12e4f 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -94,7 +94,13 @@ {% if item.is_expired %} {% trans "Expired" %} {% else %} + {% if roles.stock.change %} + + {% endif %} {% stock_status_label item.status large=True %} + {% if roles.stock.change %} + + {% endif %} {% if item.is_stale %} {% trans "Stale" %} {% endif %} @@ -453,6 +459,7 @@ $("#print-label").click(function() { printStockItemLabels([{{ item.pk }}]); }); +{% if roles.stock.change %} $("#stock-duplicate").click(function() { createNewStockItem({ follow: true, @@ -472,6 +479,18 @@ $("#stock-edit").click(function () { ); }); +$('#stock-edit-status').click(function () { + launchModalForm( + "{% url 'stock-item-edit-status' item.id %}", + { + reload: true, + submit_text: '{% trans "Save" %}', + } + ); +}); + +{% endif %} + $("#show-qr-code").click(function() { launchModalForm("{% url 'stock-item-qr' item.id %}", { diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index fe5472003f..dbdbdda317 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -4,7 +4,7 @@ URL lookup for Stock app from django.conf.urls import url, include -from . import views +from stock import views location_urls = [ @@ -24,6 +24,7 @@ location_urls = [ ] stock_item_detail_urls = [ + url(r'^edit_status/', views.StockItemEditStatus.as_view(), name='stock-item-edit-status'), url(r'^edit/', views.StockItemEdit.as_view(), name='stock-item-edit'), url(r'^convert/', views.StockItemConvert.as_view(), name='stock-item-convert'), url(r'^serialize/', views.StockItemSerialize.as_view(), name='stock-item-serialize'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 0984405055..38757d4cf2 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1212,6 +1212,16 @@ class StockAdjust(AjaxView, FormMixin): return _("Deleted {n} stock items").format(n=count) +class StockItemEditStatus(AjaxUpdateView): + """ + View for editing stock item status field + """ + + model = StockItem + form_class = StockForms.EditStockItemStatusForm + ajax_form_title = _('Edit Stock Item Status') + + class StockItemEdit(AjaxUpdateView): """ View for editing details of a single StockItem diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index f3f1c7a6bd..a0601aeb13 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -1097,6 +1097,16 @@ function loadStockTrackingTable(table, options) { // Status information if (details.status) { + html += `{% trans "Status" %}`; + + html += ''; + html += stockStatusDisplay( + details.status, + { + classes: 'float-right', + } + ); + html += ''; } @@ -1147,6 +1157,8 @@ function loadStockTrackingTable(table, options) { } }); + /* + // 2021-05-11 - Ability to edit or delete StockItemTracking entries is now removed cols.push({ sortable: false, formatter: function(value, row, index, field) { @@ -1161,6 +1173,7 @@ function loadStockTrackingTable(table, options) { } } }); + */ table.inventreeTable({ method: 'get', diff --git a/InvenTree/templates/js/table_filters.js b/InvenTree/templates/js/table_filters.js index 775f0d9803..5f516e9419 100644 --- a/InvenTree/templates/js/table_filters.js +++ b/InvenTree/templates/js/table_filters.js @@ -3,6 +3,7 @@ {% load inventree_extras %} {% include "status_codes.html" with label='stock' options=StockStatus.list %} +{% include "status_codes.html" with label='stockHistory' options=StockHistoryCode.list %} {% include "status_codes.html" with label='build' options=BuildStatus.list %} {% include "status_codes.html" with label='purchaseOrder' options=PurchaseOrderStatus.list %} {% include "status_codes.html" with label='salesOrder' options=SalesOrderStatus.list %} diff --git a/InvenTree/templates/status_codes.html b/InvenTree/templates/status_codes.html index f032f97309..e7bc2e951c 100644 --- a/InvenTree/templates/status_codes.html +++ b/InvenTree/templates/status_codes.html @@ -14,7 +14,7 @@ var {{ label }}Codes = { * Uses the values specified in "status_codes.py" * This function is generated by the "status_codes.html" template */ -function {{ label }}StatusDisplay(key) { +function {{ label }}StatusDisplay(key, options={}) { key = String(key); @@ -31,5 +31,11 @@ function {{ label }}StatusDisplay(key) { label = ''; } - return `${value}`; + var classes = `label ${label}`; + + if (options.classes) { + classes += ' ' + options.classes; + } + + return `${value}`; }