mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Merge remote-tracking branch 'inventree/master'
This commit is contained in:
		| @@ -65,7 +65,12 @@ function updateStock(items, options={}) { | ||||
|         html += '<tr>'; | ||||
|  | ||||
|         html += '<td>' + item.part.name + '</td>'; | ||||
|  | ||||
|         if (item.location) { | ||||
|             html += '<td>' + item.location.name + '</td>'; | ||||
|         } else { | ||||
|             html += '<td><i>No location set</i></td>'; | ||||
|         } | ||||
|         html += "<td><input class='form-control' "; | ||||
|         html += "value='" + vCur + "' "; | ||||
|         html += "min='" + vMin + "' "; | ||||
| @@ -144,9 +149,7 @@ function updateStock(items, options={}) { | ||||
|                             method: 'post', | ||||
|                         }).then(function(response) { | ||||
|                             closeModal(modal); | ||||
|                             if (options.success) { | ||||
|                                 options.success(); | ||||
|                             } | ||||
|                             afterForm(response, options); | ||||
|                         }).fail(function(xhr, status, error) { | ||||
|                             alert(error); | ||||
|                         }); | ||||
| @@ -220,34 +223,28 @@ function moveStockItems(items, options) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     function doMove(location, parts) { | ||||
|     function doMove(location, parts, notes) { | ||||
|         inventreeUpdate("/api/stock/move/", | ||||
|             { | ||||
|                 location: location, | ||||
|                             'parts[]': parts | ||||
|                 'parts[]': parts, | ||||
|                 'notes': notes, | ||||
|             }, | ||||
|             { | ||||
|                             success: function(response) { | ||||
|                 method: 'post', | ||||
|             }).then(function(response) { | ||||
|                 closeModal(modal); | ||||
|                                 if (options.success) { | ||||
|                                     options.success(); | ||||
|                                 } | ||||
|                             }, | ||||
|                             error: function(error) { | ||||
|                                 alert('error!:\n' + error); | ||||
|                             }, | ||||
|                             method: 'post' | ||||
|                 afterForm(response, options); | ||||
|             }).fail(function(xhr, status, error) { | ||||
|                 alert(error); | ||||
|             }); | ||||
|     } | ||||
|          | ||||
|  | ||||
|     getStockLocations({}, | ||||
|     { | ||||
|         success: function(response) { | ||||
|             openModal({ | ||||
|                 modal: modal, | ||||
|                 title: "Move " + items.length + " stock items", | ||||
|                 submit_text: "Move" | ||||
|             }); | ||||
|              | ||||
|  | ||||
|             // Extact part row info | ||||
|             var parts = []; | ||||
| @@ -262,9 +259,13 @@ function moveStockItems(items, options) { | ||||
|                 html += makeOption(loc.pk, loc.name + ' - <i>' + loc.description + '</i>'); | ||||
|             } | ||||
|  | ||||
|             html += "</select><br><hr>"; | ||||
|             html += "</select><br>"; | ||||
|  | ||||
|             html += "The following stock items will be moved:<br><ul class='list-group'>\n"; | ||||
|             html += "<hr><input type='text' id='notes' placeholder='Notes'/>"; | ||||
|  | ||||
|             html += "<p class='warning-msg' id='note-warning'><i>Note field must be filled</i></p>"; | ||||
|  | ||||
|             html += "<hr>The following stock items will be moved:<br><ul class='list-group'>\n"; | ||||
|  | ||||
|             for (i = 0; i < items.length; i++) { | ||||
|                 parts.push(items[i].pk); | ||||
| @@ -280,13 +281,29 @@ function moveStockItems(items, options) { | ||||
|  | ||||
|             html += "</ul>\n"; | ||||
|  | ||||
|             modalSetContent(modal, html); | ||||
|             openModal({ | ||||
|                 modal: modal, | ||||
|                 title: "Move " + items.length + " stock items", | ||||
|                 submit_text: "Move", | ||||
|                 content: html | ||||
|             }); | ||||
|  | ||||
|             //modalSetContent(modal, html); | ||||
|             attachSelect(modal); | ||||
|  | ||||
|             $(modal).find('#note-warning').hide(); | ||||
|  | ||||
|             modalSubmit(modal, function() { | ||||
|                 var locId = $(modal).find("#stock-location").val(); | ||||
|  | ||||
|                 doMove(locId, parts); | ||||
|                 var notes = $(modal).find('#notes').val(); | ||||
|  | ||||
|                 if (!notes) { | ||||
|                     $(modal).find('#note-warning').show(); | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 doMove(locId, parts, notes); | ||||
|             }); | ||||
|         }, | ||||
|         error: function(error) { | ||||
| @@ -358,7 +375,7 @@ function loadStockTable(table, options) { | ||||
|                         return renderLink(row.location.pathstring, row.location.url); | ||||
|                     } | ||||
|                     else { | ||||
|                         return ''; | ||||
|                         return '<i>No stock location set</i>'; | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
| @@ -383,4 +400,94 @@ function loadStockTable(table, options) { | ||||
|     if (options.buttons) { | ||||
|         linkButtonsToSelection(table, options.buttons); | ||||
|     } | ||||
| }; | ||||
| } | ||||
|  | ||||
|  | ||||
| function loadStockTrackingTable(table, options) { | ||||
|  | ||||
|     var cols = [ | ||||
|         { | ||||
|             field: 'pk', | ||||
|             visible: false, | ||||
|         }, | ||||
|         { | ||||
|             field: 'date', | ||||
|             title: 'Date', | ||||
|             sortable: true, | ||||
|             formatter: function(value, row, index, field) { | ||||
|                 var m = moment(value); | ||||
|                 if (m.isValid()) { | ||||
|                     var html = m.format('dddd MMMM Do YYYY') + '<br>' + m.format('h:mm a'); | ||||
|                     return html; | ||||
|                 } | ||||
|  | ||||
|                 return 'N/A'; | ||||
|             } | ||||
|         }, | ||||
|     ]; | ||||
|  | ||||
|     // If enabled, provide a link to the referenced StockItem | ||||
|     if (options.partColumn) { | ||||
|         cols.push({ | ||||
|             field: 'item', | ||||
|             title: 'Stock Item', | ||||
|             sortable: true, | ||||
|             formatter: function(value, row, index, field) { | ||||
|                 return renderLink(value.part_name, value.url); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Stock transaction description | ||||
|     cols.push({ | ||||
|         field: 'title', | ||||
|         title: 'Description', | ||||
|         sortable: true, | ||||
|         formatter: function(value, row, index, field) { | ||||
|             var html = "<b>" + value + "</b>"; | ||||
|  | ||||
|             if (row.notes) { | ||||
|                 html += "<br><i>" + row.notes + "</i>"; | ||||
|             } | ||||
|  | ||||
|             return html; | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     cols.push({ | ||||
|         field: 'quantity', | ||||
|         title: 'Quantity', | ||||
|     }); | ||||
|  | ||||
|     cols.push({ | ||||
|         sortable: true, | ||||
|         field: 'user', | ||||
|         title: 'User', | ||||
|         formatter: function(value, row, index, field) { | ||||
|             if (value) | ||||
|             { | ||||
|                 // TODO - Format the user's first and last names | ||||
|                 return value.username; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 return "No user information"; | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     table.bootstrapTable({ | ||||
|         sortable: true, | ||||
|         search: true, | ||||
|         method: 'get', | ||||
|         rememberOrder: true, | ||||
|         queryParams: options.params, | ||||
|         columns: cols, | ||||
|         pagination: true, | ||||
|         url: options.url, | ||||
|     }); | ||||
|  | ||||
|     if (options.buttons) { | ||||
|         linkButtonsToSelection(table, options.buttons); | ||||
|     } | ||||
| } | ||||
| @@ -111,17 +111,22 @@ class StockStocktake(APIView): | ||||
|         if 'notes' in request.data: | ||||
|             notes = request.data['notes'] | ||||
|  | ||||
|         n = 0 | ||||
|  | ||||
|         for item in items: | ||||
|             quantity = int(item['quantity']) | ||||
|  | ||||
|             if action == u'stocktake': | ||||
|                 item['item'].stocktake(quantity, request.user, notes=notes) | ||||
|                 if item['item'].stocktake(quantity, request.user, notes=notes): | ||||
|                     n += 1 | ||||
|             elif action == u'remove': | ||||
|                 item['item'].take_stock(quantity, request.user, notes=notes) | ||||
|                 if item['item'].take_stock(quantity, request.user, notes=notes): | ||||
|                     n += 1 | ||||
|             elif action == u'add': | ||||
|                 item['item'].add_stock(quantity, request.user, notes=notes) | ||||
|                 if item['item'].add_stock(quantity, request.user, notes=notes): | ||||
|                     n += 1 | ||||
|  | ||||
|         return Response({'success': 'success'}) | ||||
|         return Response({'success': 'Updated stock for {n} items'.format(n=n)}) | ||||
|  | ||||
|  | ||||
| class StockMove(APIView): | ||||
| @@ -153,6 +158,9 @@ class StockMove(APIView): | ||||
|  | ||||
|         errors = [] | ||||
|  | ||||
|         if u'notes' not in data: | ||||
|             errors.append({'notes': 'Notes field must be supplied'}) | ||||
|  | ||||
|         for pid in part_list: | ||||
|             try: | ||||
|                 part = StockItem.objects.get(pk=pid) | ||||
| @@ -163,12 +171,15 @@ class StockMove(APIView): | ||||
|         if len(errors) > 0: | ||||
|             raise ValidationError(errors) | ||||
|  | ||||
|         n = 0 | ||||
|  | ||||
|         for part in parts: | ||||
|             part.move(location, request.user) | ||||
|             if part.move(location, data.get('notes'), request.user): | ||||
|                 n += 1 | ||||
|  | ||||
|         return Response({'success': 'Moved {n} parts to {loc}'.format( | ||||
|             n=len(parts), | ||||
|             loc=location.name | ||||
|             n=n, | ||||
|             loc=str(location) | ||||
|         )}) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -64,7 +64,9 @@ class EditStockItemForm(HelperForm): | ||||
|         model = StockItem | ||||
|  | ||||
|         fields = [ | ||||
|             'supplier_part', | ||||
|             'batch', | ||||
|             'status', | ||||
|             'notes' | ||||
|             'notes', | ||||
|             'URL', | ||||
|         ] | ||||
|   | ||||
| @@ -67,6 +67,7 @@ class StockItem(models.Model): | ||||
|             self.add_transaction_note( | ||||
|                 'Created stock item', | ||||
|                 None, | ||||
|                 notes="Created new stock item for part '{p}'".format(p=str(self.part)), | ||||
|                 system=True | ||||
|             ) | ||||
|  | ||||
| @@ -220,13 +221,17 @@ class StockItem(models.Model): | ||||
|     @transaction.atomic | ||||
|     def move(self, location, notes, user): | ||||
|  | ||||
|         if location.pk == self.location.pk: | ||||
|             return False  # raise forms.ValidationError("Cannot move item to its current location") | ||||
|         if location is None: | ||||
|             # TODO - Raise appropriate error (cannot move to blank location) | ||||
|             return False | ||||
|         elif self.location and (location.pk == self.location.pk): | ||||
|             # TODO - Raise appropriate error (cannot move to same location) | ||||
|             return False | ||||
|  | ||||
|         msg = "Moved to {loc} (from {src})".format( | ||||
|             loc=location.name, | ||||
|             src=self.location.name | ||||
|         ) | ||||
|         msg = "Moved to {loc}".format(loc=str(location)) | ||||
|  | ||||
|         if self.location: | ||||
|             msg += " (from {loc})".format(loc=str(self.location)) | ||||
|  | ||||
|         self.location = location | ||||
|         self.save() | ||||
| @@ -329,7 +334,8 @@ class StockItemTracking(models.Model): | ||||
|     """ | ||||
|  | ||||
|     def get_absolute_url(self): | ||||
|         return reverse('stock-tracking-detail', kwargs={'pk': self.id}) | ||||
|         return '/stock/track/{pk}'.format(pk=self.id) | ||||
|         # return reverse('stock-tracking-detail', kwargs={'pk': self.id}) | ||||
|  | ||||
|     # Stock item | ||||
|     item = models.ForeignKey(StockItem, on_delete=models.CASCADE, | ||||
|   | ||||
| @@ -24,31 +24,22 @@ class LocationBriefSerializer(serializers.ModelSerializer): | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class StockTrackingSerializer(serializers.ModelSerializer): | ||||
| class StockItemSerializerBrief(serializers.ModelSerializer): | ||||
|     """ | ||||
|     Provide a brief serializer for StockItem | ||||
|     """ | ||||
|  | ||||
|     url = serializers.CharField(source='get_absolute_url', read_only=True) | ||||
|  | ||||
|     user = UserSerializerBrief(many=False, read_only=True) | ||||
|     part_name = serializers.CharField(source='part.name', read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = StockItemTracking | ||||
|         model = StockItem | ||||
|         fields = [ | ||||
|             'pk', | ||||
|             'uuid', | ||||
|             'url', | ||||
|             'item', | ||||
|             'date', | ||||
|             'title', | ||||
|             'notes', | ||||
|             'quantity', | ||||
|             'user', | ||||
|             'system', | ||||
|         ] | ||||
|  | ||||
|         read_only_fields = [ | ||||
|             'date', | ||||
|             'user', | ||||
|             'system', | ||||
|             'quantity', | ||||
|             'part_name', | ||||
|         ] | ||||
|  | ||||
|  | ||||
| @@ -118,3 +109,33 @@ class LocationSerializer(serializers.ModelSerializer): | ||||
|             'parent', | ||||
|             'pathstring' | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class StockTrackingSerializer(serializers.ModelSerializer): | ||||
|  | ||||
|     url = serializers.CharField(source='get_absolute_url', read_only=True) | ||||
|  | ||||
|     user = UserSerializerBrief(many=False, read_only=True) | ||||
|  | ||||
|     item = StockItemSerializerBrief(many=False, read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = StockItemTracking | ||||
|         fields = [ | ||||
|             'pk', | ||||
|             'url', | ||||
|             'item', | ||||
|             'date', | ||||
|             'title', | ||||
|             'notes', | ||||
|             'quantity', | ||||
|             'user', | ||||
|             'system', | ||||
|         ] | ||||
|  | ||||
|         read_only_fields = [ | ||||
|             'date', | ||||
|             'user', | ||||
|             'system', | ||||
|             'quantity', | ||||
|         ] | ||||
|   | ||||
| @@ -121,22 +121,11 @@ | ||||
|  | ||||
| {% if item.has_tracking_info %} | ||||
|  | ||||
| <hr> | ||||
| <div class="panel-group"> | ||||
|  <div class="panel panel-default"> | ||||
|    <div class="panel-heading"> | ||||
|      <h4 class="panel-title"> | ||||
|        <a data-toggle="collapse" href="#collapse1">Stock Tracking</a><span class='badge'>{{ item.tracking_info.all|length }}</span> | ||||
|      </h4> | ||||
|    </div> | ||||
|    <div id="collapse1" class="panel-collapse collapse"> | ||||
|      <div class="panel-body"> | ||||
|          <table class='table table-condensed table-striped' id='track-table'> | ||||
|          </table> | ||||
|      </div> | ||||
|    </div> | ||||
|  </div> | ||||
| <div id='table-toolbar'>     | ||||
|     <h4>Stock Tracking Information</h4> | ||||
| </div> | ||||
| <table class='table table-condensed table-striped' id='track-table' data-toolbar='#table-toolbar'> | ||||
| </table> | ||||
| {% endif %} | ||||
| {% endblock %} | ||||
| {% block js_ready %} | ||||
| @@ -210,66 +199,14 @@ | ||||
|                          }); | ||||
|     }); | ||||
|  | ||||
|     $('#track-table').bootstrapTable({ | ||||
|         sortable: true, | ||||
|         search: true, | ||||
|         method: 'get', | ||||
|         queryParams: function(p) { | ||||
|     loadStockTrackingTable($("#track-table"), { | ||||
|         params: function(p) { | ||||
|             return { | ||||
|                 ordering: '-date', | ||||
|                 item: {{ item.pk }}, | ||||
|             } | ||||
|             }; | ||||
|         }, | ||||
|         columns: [ | ||||
|             { | ||||
|                 field: 'date', | ||||
|                 title: 'Date', | ||||
|                 sortable: true, | ||||
|                 formatter: function(value, row, index, field) { | ||||
|                     var m = moment(value); | ||||
|                     if (m.isValid()) { | ||||
|                         var html = m.format('dddd MMMM Do YYYY') + '<br>' + m.format('h:mm a'); | ||||
|                         return html; | ||||
|                     } | ||||
|  | ||||
|                     return 'N/A'; | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 field: 'title', | ||||
|                 title: 'Description', | ||||
|                 sortable: true, | ||||
|                 formatter: function(value, row, index, field) { | ||||
|                     var html = "<b>" + value + "</b>"; | ||||
|  | ||||
|                     if (row.notes) { | ||||
|                         html += "<br><i>" + row.notes + "</i>"; | ||||
|                     } | ||||
|  | ||||
|                     return html; | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 field: 'quantity', | ||||
|                 title: 'Quantity', | ||||
|             }, | ||||
|             { | ||||
|                 sortable: true, | ||||
|                 field: 'user', | ||||
|                 title: 'User', | ||||
|                 formatter: function(value, row, index, field) { | ||||
|                     if (value) | ||||
|                     { | ||||
|                         // TODO - Format the user's first and last names | ||||
|                         return value.username; | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         return "No user information"; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         ], | ||||
|         url: "{% url 'api-stock-track' %}",  | ||||
|     }) | ||||
|     }); | ||||
|  | ||||
| {% endblock %} | ||||
							
								
								
									
										28
									
								
								InvenTree/stock/templates/stock/tracking.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								InvenTree/stock/templates/stock/tracking.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| {% extends "stock/stock_app_base.html" %} | ||||
| {% load static %} | ||||
|  | ||||
| {% block content %} | ||||
|  | ||||
| <h3>Stock list here!</h3> | ||||
|  | ||||
| <table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='tracking-table'> | ||||
| </table> | ||||
|  | ||||
| {% include 'modals.html' %} | ||||
|  | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js_ready %} | ||||
| {{ block.super }} | ||||
|  | ||||
|     loadStockTrackingTable($("#tracking-table"), { | ||||
|         params: function(p) { | ||||
|             return { | ||||
|                 ordering: '-date', | ||||
|             }; | ||||
|         }, | ||||
|         partColumn: true, | ||||
|         url: "{% url 'api-stock-track' %}",  | ||||
|     }); | ||||
|  | ||||
| {% endblock %} | ||||
| @@ -28,6 +28,8 @@ stock_urls = [ | ||||
|  | ||||
|     url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'), | ||||
|  | ||||
|     url(r'^track/?', views.StockTrackingIndex.as_view(), name='stock-tracking-list'), | ||||
|  | ||||
|     # Individual stock items | ||||
|     url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)), | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ from django.forms.models import model_to_dict | ||||
| from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView | ||||
|  | ||||
| from part.models import Part | ||||
| from .models import StockItem, StockLocation | ||||
| from .models import StockItem, StockLocation, StockItemTracking | ||||
|  | ||||
| from .forms import EditStockLocationForm | ||||
| from .forms import CreateStockItemForm | ||||
| @@ -248,3 +248,13 @@ class StockItemStocktake(AjaxUpdateView): | ||||
|         } | ||||
|  | ||||
|         return self.renderJsonResponse(request, form, data) | ||||
|  | ||||
|  | ||||
| class StockTrackingIndex(ListView): | ||||
|     """ | ||||
|     StockTrackingIndex provides a page to display StockItemTracking objects | ||||
|     """ | ||||
|  | ||||
|     model = StockItemTracking | ||||
|     template_name = 'stock/tracking.html' | ||||
|     context_object_name = 'items' | ||||
|   | ||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							| @@ -8,7 +8,7 @@ clean: | ||||
| 	rm -f .coverage | ||||
|  | ||||
| style: | ||||
| 	flake8 InvenTree --ignore=C901,E501 | ||||
| 	flake8 InvenTree | ||||
|  | ||||
| test: | ||||
| 	python InvenTree/manage.py check | ||||
|   | ||||
		Reference in New Issue
	
	Block a user