mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Merge branch 'inventree:master' into matmair/issue2279
This commit is contained in:
		| @@ -16,12 +16,6 @@ | ||||
| <div class='panel panel-inventree'> | ||||
|     <div class='panel-content'> | ||||
|         {% include "search_form.html" with query_text=query %} | ||||
|         {% if query %} | ||||
|         {% else %} | ||||
|         <div id='empty-search-query'> | ||||
|             <h4><em>{% trans "Enter a search query" %}</em></h4> | ||||
|         </div> | ||||
|         {% endif %} | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| <div id='attachment-buttons'> | ||||
|     <div class='btn-group' role='group'> | ||||
|         {% include "filter_list.html" with id="related" %} | ||||
|         {% include "filter_list.html" with id="attachments" %} | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
|   | ||||
| @@ -207,6 +207,11 @@ function showApiError(xhr, url) { | ||||
|         title = '{% trans "Error 404: Resource Not Found" %}'; | ||||
|         message = '{% trans "The requested resource could not be located on the server" %}'; | ||||
|         break; | ||||
|     // Method not allowed | ||||
|     case 405: | ||||
|         title = '{% trans "Error 405: Method Not Allowed" %}'; | ||||
|         message = '{% trans "HTTP method not allowed at URL" %}'; | ||||
|         break; | ||||
|     // Timeout | ||||
|     case 408: | ||||
|         title = '{% trans "Error 408: Timeout" %}'; | ||||
|   | ||||
| @@ -67,6 +67,8 @@ function loadAttachmentTable(url, options) { | ||||
|  | ||||
|     var table = options.table || '#attachment-table'; | ||||
|  | ||||
|     setupFilterList('attachments', $(table), '#filter-list-attachments'); | ||||
|  | ||||
|     addAttachmentButtonCallbacks(url, options.fields || {}); | ||||
|  | ||||
|     $(table).inventreeTable({ | ||||
|   | ||||
| @@ -661,7 +661,7 @@ function loadBomTable(table, options={}) { | ||||
|             if (!row.inherited) { | ||||
|                 return yesNoLabel(false); | ||||
|             } else if (row.part == options.parent_id) { | ||||
|                 return '{% trans "Inherited" %}'; | ||||
|                 return yesNoLabel(true); | ||||
|             } else { | ||||
|                 // If this BOM item is inherited from a parent part | ||||
|                 return renderLink( | ||||
|   | ||||
| @@ -380,6 +380,7 @@ function loadCompanyTable(table, url, options={}) { | ||||
|         url: url, | ||||
|         method: 'get', | ||||
|         queryParams: filters, | ||||
|         original: params, | ||||
|         groupBy: false, | ||||
|         sidePagination: 'server', | ||||
|         formatNoMatches: function() { | ||||
| @@ -463,7 +464,9 @@ function loadManufacturerPartTable(table, url, options) { | ||||
|         filters[key] = params[key]; | ||||
|     } | ||||
|  | ||||
|     setupFilterList('manufacturer-part', $(table)); | ||||
|     var filterTarget = options.filterTarget || '#filter-list-manufacturer-part'; | ||||
|  | ||||
|     setupFilterList('manufacturer-part', $(table), filterTarget); | ||||
|  | ||||
|     $(table).inventreeTable({ | ||||
|         url: url, | ||||
|   | ||||
| @@ -21,6 +21,7 @@ | ||||
| */ | ||||
|  | ||||
| /* exported | ||||
|     duplicateBom, | ||||
|     duplicatePart, | ||||
|     editCategory, | ||||
|     editPart, | ||||
| @@ -39,6 +40,7 @@ | ||||
|     loadStockPricingChart, | ||||
|     partStockLabel, | ||||
|     toggleStar, | ||||
|     validateBom, | ||||
| */ | ||||
|  | ||||
| /* Part API functions | ||||
| @@ -428,6 +430,59 @@ function toggleStar(options) { | ||||
| } | ||||
|  | ||||
|  | ||||
| /* Validate a BOM */ | ||||
| function validateBom(part_id, options={}) { | ||||
|  | ||||
|     var html = ` | ||||
|     <div class='alert alert-block alert-success'> | ||||
|     {% trans "Validating the BOM will mark each line item as valid" %} | ||||
|     </div> | ||||
|     `; | ||||
|  | ||||
|     constructForm(`/api/part/${part_id}/bom-validate/`, { | ||||
|         method: 'PUT', | ||||
|         fields: { | ||||
|             valid: {}, | ||||
|         }, | ||||
|         preFormContent: html, | ||||
|         title: '{% trans "Validate Bill of Materials" %}', | ||||
|         reload: options.reload, | ||||
|         onSuccess: function(response) { | ||||
|             showMessage('{% trans "Validated Bill of Materials" %}'); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
|  | ||||
| /* Duplicate a BOM */ | ||||
| function duplicateBom(part_id, options={}) { | ||||
|  | ||||
|     constructForm(`/api/part/${part_id}/bom-copy/`, { | ||||
|         method: 'POST', | ||||
|         fields: { | ||||
|             part: { | ||||
|                 icon: 'fa-shapes', | ||||
|                 filters: { | ||||
|                     assembly: true, | ||||
|                     exclude_tree: part_id, | ||||
|                 } | ||||
|             }, | ||||
|             include_inherited: {}, | ||||
|             remove_existing: {}, | ||||
|             skip_invalid: {}, | ||||
|         }, | ||||
|         confirm: true, | ||||
|         title: '{% trans "Copy Bill of Materials" %}', | ||||
|         onSuccess: function(response) { | ||||
|             if (options.success) { | ||||
|                 options.success(response); | ||||
|             } | ||||
|         }, | ||||
|     }); | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| function partStockLabel(part, options={}) { | ||||
|  | ||||
|     if (part.in_stock) { | ||||
| @@ -621,7 +676,9 @@ function loadPartParameterTable(table, url, options) { | ||||
|         filters[key] = params[key]; | ||||
|     } | ||||
|  | ||||
|     // setupFilterList("#part-parameters", $(table)); | ||||
|     var filterTarget = options.filterTarget || '#filter-list-parameters'; | ||||
|  | ||||
|     setupFilterList('part-parameters', $(table), filterTarget); | ||||
|  | ||||
|     $(table).inventreeTable({ | ||||
|         url: url, | ||||
| @@ -727,7 +784,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { | ||||
|     options.params.part_detail = true; | ||||
|     options.params.order_detail = true; | ||||
|      | ||||
|     var filters = loadTableFilters('partpurchaseorders'); | ||||
|     var filters = loadTableFilters('purchaseorderlineitem'); | ||||
|  | ||||
|     for (var key in options.params) { | ||||
|         filters[key] = options.params[key]; | ||||
| @@ -871,7 +928,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { | ||||
|                     if (row.received >= row.quantity) { | ||||
|                         // Already recevied | ||||
|                         return `<span class='badge bg-success rounded-pill'>{% trans "Received" %}</span>`; | ||||
|                     } else { | ||||
|                     } else if (row.order_detail && row.order_detail.status == {{ PurchaseOrderStatus.PLACED }}) { | ||||
|                         var html = `<div class='btn-group' role='group'>`; | ||||
|                         var pk = row.pk; | ||||
|  | ||||
| @@ -879,6 +936,8 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { | ||||
|  | ||||
|                         html += `</div>`; | ||||
|                         return html; | ||||
|                     } else { | ||||
|                         return ''; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -52,6 +52,7 @@ | ||||
|     loadStockTestResultsTable, | ||||
|     loadStockTrackingTable, | ||||
|     loadTableFilters, | ||||
|     mergeStockItems, | ||||
|     removeStockRow, | ||||
|     serializeStockItem, | ||||
|     stockItemFields, | ||||
| @@ -595,17 +596,17 @@ function assignStockToCustomer(items, options={}) { | ||||
|         buttons += '</div>'; | ||||
|  | ||||
|         html += ` | ||||
|             <tr id='stock_item_${pk}' class='stock-item'row'> | ||||
|                 <td id='part_${pk}'>${thumbnail} ${part.full_name}</td> | ||||
|                 <td id='stock_${pk}'> | ||||
|                     <div id='div_id_items_item_${pk}'> | ||||
|                         ${quantity} | ||||
|                         <div id='errors-items_item_${pk}'></div> | ||||
|                     </div> | ||||
|                 </td> | ||||
|                 <td id='location_${pk}'>${location}</td> | ||||
|                 <td id='buttons_${pk}'>${buttons}</td> | ||||
|             </tr> | ||||
|         <tr id='stock_item_${pk}' class='stock-item-row'> | ||||
|             <td id='part_${pk}'>${thumbnail} ${part.full_name}</td> | ||||
|             <td id='stock_${pk}'> | ||||
|                 <div id='div_id_items_item_${pk}'> | ||||
|                     ${quantity} | ||||
|                     <div id='errors-items_item_${pk}'></div> | ||||
|                 </div> | ||||
|             </td> | ||||
|             <td id='location_${pk}'>${location}</td> | ||||
|             <td id='buttons_${pk}'>${buttons}</td> | ||||
|         </tr> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
| @@ -615,13 +616,13 @@ function assignStockToCustomer(items, options={}) { | ||||
|         method: 'POST', | ||||
|         preFormContent: html, | ||||
|         fields: { | ||||
|             'customer': { | ||||
|             customer: { | ||||
|                 value: options.customer, | ||||
|                 filters: { | ||||
|                     is_customer: true, | ||||
|                 }, | ||||
|             }, | ||||
|             'notes': {}, | ||||
|             notes: {}, | ||||
|         }, | ||||
|         confirm: true, | ||||
|         confirmMessage: '{% trans "Confirm stock assignment" %}', | ||||
| @@ -694,6 +695,184 @@ function assignStockToCustomer(items, options={}) { | ||||
| } | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Merge multiple stock items together | ||||
|  */ | ||||
| function mergeStockItems(items, options={}) { | ||||
|  | ||||
|     // Generate HTML content for the form | ||||
|     var html = ` | ||||
|     <div class='alert alert-block alert-danger'> | ||||
|     <h5>{% trans "Warning: Merge operation cannot be reversed" %}</h5> | ||||
|     <strong>{% trans "Some information will be lost when merging stock items" %}:</strong> | ||||
|     <ul> | ||||
|         <li>{% trans "Stock transaction history will be deleted for merged items" %}</li> | ||||
|         <li>{% trans "Supplier part information will be deleted for merged items" %}</li> | ||||
|     </ul> | ||||
|     </div> | ||||
|     `; | ||||
|  | ||||
|     html += ` | ||||
|     <table class='table table-striped table-condensed' id='stock-merge-table'> | ||||
|     <thead> | ||||
|         <tr> | ||||
|             <th>{% trans "Part" %}</th> | ||||
|             <th>{% trans "Stock Item" %}</th> | ||||
|             <th>{% trans "Location" %}</th> | ||||
|             <th></th> | ||||
|         </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|     `; | ||||
|  | ||||
|     // Keep track of how many "locations" there are | ||||
|     var locations = []; | ||||
|  | ||||
|     for (var idx = 0; idx < items.length; idx++) { | ||||
|         var item = items[idx]; | ||||
|  | ||||
|         var pk = item.pk; | ||||
|  | ||||
|         if (item.location && !locations.includes(item.location)) { | ||||
|             locations.push(item.location); | ||||
|         } | ||||
|  | ||||
|         var part = item.part_detail; | ||||
|         var location = locationDetail(item, false); | ||||
|  | ||||
|         var thumbnail = thumbnailImage(part.thumbnail || part.image); | ||||
|  | ||||
|         var quantity = ''; | ||||
|  | ||||
|         if (item.serial && item.quantity == 1) { | ||||
|             quantity = `{% trans "Serial" %}: ${item.serial}`; | ||||
|         } else { | ||||
|             quantity = `{% trans "Quantity" %}: ${item.quantity}`; | ||||
|         } | ||||
|  | ||||
|         quantity += stockStatusDisplay(item.status, {classes: 'float-right'}); | ||||
|  | ||||
|         var buttons = `<div class='btn-group' role='group'>`; | ||||
|  | ||||
|         buttons += makeIconButton( | ||||
|             'fa-times icon-red', | ||||
|             'button-stock-item-remove', | ||||
|             pk, | ||||
|             '{% trans "Remove row" %}', | ||||
|         ); | ||||
|  | ||||
|         html += ` | ||||
|         <tr id='stock_item_${pk}' class='stock-item-row'> | ||||
|             <td id='part_${pk}'>${thumbnail} ${part.full_name}</td> | ||||
|             <td id='stock_${pk}'> | ||||
|                 <div id='div_id_items_item_${pk}'> | ||||
|                     ${quantity} | ||||
|                     <div id='errors-items_item_${pk}'></div> | ||||
|                 </div> | ||||
|             </td> | ||||
|             <td id='location_${pk}'>${location}</td> | ||||
|             <td id='buttons_${pk}'>${buttons}</td> | ||||
|         </tr> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     html += '</tbody></table>'; | ||||
|  | ||||
|     var location = locations.length == 1 ? locations[0] : null; | ||||
|  | ||||
|     constructForm('{% url "api-stock-merge" %}', { | ||||
|         method: 'POST', | ||||
|         preFormContent: html, | ||||
|         fields: { | ||||
|             location: { | ||||
|                 value: location, | ||||
|                 icon: 'fa-sitemap', | ||||
|             }, | ||||
|             notes: {}, | ||||
|             allow_mismatched_suppliers: {}, | ||||
|             allow_mismatched_status: {}, | ||||
|         }, | ||||
|         confirm: true, | ||||
|         confirmMessage: '{% trans "Confirm stock item merge" %}', | ||||
|         title: '{% trans "Merge Stock Items" %}', | ||||
|         afterRender: function(fields, opts) { | ||||
|             // Add button callbacks to remove rows | ||||
|             $(opts.modal).find('.button-stock-item-remove').click(function() { | ||||
|                 var pk = $(this).attr('pk'); | ||||
|  | ||||
|                 $(opts.modal).find(`#stock_item_${pk}`).remove(); | ||||
|             }); | ||||
|         }, | ||||
|         onSubmit: function(fields, opts) { | ||||
|  | ||||
|             // Extract data elements from the form | ||||
|             var data = { | ||||
|                 items: [], | ||||
|             }; | ||||
|  | ||||
|             var item_pk_values = []; | ||||
|  | ||||
|             items.forEach(function(item) { | ||||
|                 var pk = item.pk; | ||||
|  | ||||
|                 // Does the row still exist in the form? | ||||
|                 var row = $(opts.modal).find(`#stock_item_${pk}`); | ||||
|  | ||||
|                 if (row.exists()) { | ||||
|                     item_pk_values.push(pk); | ||||
|  | ||||
|                     data.items.push({ | ||||
|                         item: pk, | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             var extra_fields = [ | ||||
|                 'location', | ||||
|                 'notes', | ||||
|                 'allow_mismatched_suppliers', | ||||
|                 'allow_mismatched_status', | ||||
|             ]; | ||||
|  | ||||
|             extra_fields.forEach(function(field) { | ||||
|                 data[field] = getFormFieldValue(field, fields[field], opts); | ||||
|             }); | ||||
|  | ||||
|             opts.nested = { | ||||
|                 'items': item_pk_values | ||||
|             }; | ||||
|  | ||||
|             // Submit the form data | ||||
|             inventreePut( | ||||
|                 '{% url "api-stock-merge" %}', | ||||
|                 data, | ||||
|                 { | ||||
|                     method: 'POST', | ||||
|                     success: function(response) { | ||||
|                         $(opts.modal).modal('hide'); | ||||
|  | ||||
|                         if (options.success) { | ||||
|                             options.success(response); | ||||
|                         } | ||||
|                     }, | ||||
|                     error: function(xhr) { | ||||
|                         switch (xhr.status) { | ||||
|                         case 400: | ||||
|                             handleFormErrors(xhr.responseJSON, fields, opts); | ||||
|                             break; | ||||
|                         default: | ||||
|                             $(opts.modal).modal('hide'); | ||||
|                             showApiError(xhr, opts.url); | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             ); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Perform stock adjustments | ||||
|  */ | ||||
| @@ -1289,7 +1468,7 @@ function loadStockTable(table, options) { | ||||
|  | ||||
|     var params = options.params || {}; | ||||
|  | ||||
|     var filterListElement = options.filterList || '#filter-list-stock'; | ||||
|     var filterTarget = options.filterTarget || '#filter-list-stock'; | ||||
|  | ||||
|     var filters = {}; | ||||
|  | ||||
| @@ -1305,7 +1484,7 @@ function loadStockTable(table, options) { | ||||
|         original[k] = params[k]; | ||||
|     } | ||||
|  | ||||
|     setupFilterList(filterKey, table, filterListElement); | ||||
|     setupFilterList(filterKey, table, filterTarget); | ||||
|  | ||||
|     // Override the default values, or add new ones | ||||
|     for (var key in params) { | ||||
| @@ -1458,7 +1637,7 @@ function loadStockTable(table, options) { | ||||
|             } | ||||
|  | ||||
|             if (row.quantity <= 0) { | ||||
|                 html += `<span class='badge rounded-pill bg-danger'>{% trans "Depleted" %}</span>`; | ||||
|                 html += `<span class='badge badge-right rounded-pill bg-danger'>{% trans "Depleted" %}</span>`; | ||||
|             } | ||||
|  | ||||
|             return html; | ||||
| @@ -1875,6 +2054,20 @@ function loadStockTable(table, options) { | ||||
|         stockAdjustment('move'); | ||||
|     }); | ||||
|  | ||||
|     $('#multi-item-merge').click(function() { | ||||
|         var items = $(table).bootstrapTable('getSelections'); | ||||
|  | ||||
|         mergeStockItems(items, { | ||||
|             success: function(response) { | ||||
|                 $(table).bootstrapTable('refresh'); | ||||
|  | ||||
|                 showMessage('{% trans "Merged stock items" %}', { | ||||
|                     style: 'success', | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     $('#multi-item-assign').click(function() { | ||||
|  | ||||
|         var items = $(table).bootstrapTable('getSelections'); | ||||
|   | ||||
| @@ -381,6 +381,24 @@ function getAvailableTableFilters(tableKey) { | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     // Filters for "company" table | ||||
|     if (tableKey == 'company') { | ||||
|         return { | ||||
|             is_manufacturer: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Manufacturer" %}', | ||||
|             }, | ||||
|             is_supplier: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Supplier" %}', | ||||
|             }, | ||||
|             is_customer: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Customer" %}', | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     // Filters for the "Parts" table | ||||
|     if (tableKey == 'parts') { | ||||
|         return { | ||||
|   | ||||
| @@ -49,6 +49,7 @@ | ||||
|                     <li><a class='dropdown-item' href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove stock" %}</a></li> | ||||
|                     <li><a class='dropdown-item' href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li> | ||||
|                     <li><a class='dropdown-item' href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li> | ||||
|                     <li><a class='dropdown-item' href='#' id='multi-item-merge' title='{% trans "Merge selected stock items" %}'><span class='fas fa-object-group'></span> {% trans "Merge stock" %}</a></li> | ||||
|                     <li><a class='dropdown-item' href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li> | ||||
|                     <li><a class='dropdown-item' href='#' id='multi-item-assign' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li> | ||||
|                     <li><a class='dropdown-item' href='#' id='multi-item-set-status' title='{% trans "Change status" %}'><span class='fas fa-exclamation-circle'></span> {% trans "Change stock status" %}</a></li> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user