mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Merge branch 'inventree:master' into plugin-2037
This commit is contained in:
		| @@ -266,7 +266,7 @@ class RegistratonMixin: | ||||
|         return False | ||||
|  | ||||
|     def save_user(self, request, user, form, commit=True): | ||||
|         user = super().save_user(request, user, form, commit=commit) | ||||
|         user = super().save_user(request, user, form) | ||||
|         start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP') | ||||
|         if start_group: | ||||
|             try: | ||||
|   | ||||
| @@ -36,6 +36,13 @@ class InvenTreeMoneySerializer(MoneyField): | ||||
|     Ref: https://github.com/django-money/django-money/blob/master/djmoney/contrib/django_rest_framework/fields.py | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|  | ||||
|         kwargs["max_digits"] = kwargs.get("max_digits", 19) | ||||
|         kwargs["decimal_places"] = kwargs.get("decimal_places", 4) | ||||
|  | ||||
|         super().__init__(*args, **kwargs) | ||||
|  | ||||
|     def get_value(self, data): | ||||
|         """ | ||||
|         Test that the returned amount is a valid Decimal | ||||
| @@ -52,7 +59,7 @@ class InvenTreeMoneySerializer(MoneyField): | ||||
|                 amount = Decimal(amount) | ||||
|         except: | ||||
|             raise ValidationError({ | ||||
|                 self.field_name: _("Must be a valid number") | ||||
|                 self.field_name: [_("Must be a valid number")], | ||||
|             }) | ||||
|  | ||||
|         currency = data.get(get_currency_field_name(self.field_name), self.default_currency) | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from rest_framework import serializers | ||||
| from sql_util.utils import SubqueryCount | ||||
|  | ||||
| from InvenTree.serializers import InvenTreeModelSerializer | ||||
| from InvenTree.serializers import InvenTreeMoneySerializer | ||||
| from InvenTree.serializers import InvenTreeImageSerializerField | ||||
|  | ||||
| from part.serializers import PartBriefSerializer | ||||
| @@ -256,7 +257,11 @@ class SupplierPriceBreakSerializer(InvenTreeModelSerializer): | ||||
|  | ||||
|     quantity = serializers.FloatField() | ||||
|  | ||||
|     price = serializers.CharField() | ||||
|     price = InvenTreeMoneySerializer( | ||||
|         allow_null=True, | ||||
|         required=True, | ||||
|         label=_('Price'), | ||||
|     ) | ||||
|  | ||||
|     price_currency = serializers.ChoiceField( | ||||
|         choices=currency_code_mappings(), | ||||
|   | ||||
| @@ -154,7 +154,6 @@ class POLineItemSerializer(InvenTreeModelSerializer): | ||||
|     supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True) | ||||
|  | ||||
|     purchase_price = InvenTreeMoneySerializer( | ||||
|         max_digits=19, decimal_places=4, | ||||
|         allow_null=True | ||||
|     ) | ||||
|  | ||||
| @@ -557,8 +556,6 @@ class SOLineItemSerializer(InvenTreeModelSerializer): | ||||
|     fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True) | ||||
|      | ||||
|     sale_price = InvenTreeMoneySerializer( | ||||
|         max_digits=19, | ||||
|         decimal_places=4, | ||||
|         allow_null=True | ||||
|     ) | ||||
|  | ||||
|   | ||||
| @@ -19,9 +19,13 @@ | ||||
|     <div class='panel-content'> | ||||
|         {% if roles.sales_order.change %} | ||||
|         <div id='order-toolbar-buttons' class='btn-group' style='float: right;'> | ||||
|             <div class='btn-group'> | ||||
|                 <button type='button' class='btn btn-success' id='new-so-line'> | ||||
|                     <span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %} | ||||
|                 </button> | ||||
|                 <div class='filter-list' id='filter-list-sales-order-lines'> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         {% endif %} | ||||
|         <table class='table table-striped table-condensed' id='so-lines-table' data-toolbar='#order-toolbar-buttons'> | ||||
|   | ||||
| @@ -109,7 +109,6 @@ class PartSalePriceSerializer(InvenTreeModelSerializer): | ||||
|     quantity = serializers.FloatField() | ||||
|  | ||||
|     price = InvenTreeMoneySerializer( | ||||
|         max_digits=19, decimal_places=4, | ||||
|         allow_null=True | ||||
|     ) | ||||
|  | ||||
| @@ -134,7 +133,6 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer): | ||||
|     quantity = serializers.FloatField() | ||||
|  | ||||
|     price = InvenTreeMoneySerializer( | ||||
|         max_digits=19, decimal_places=4, | ||||
|         allow_null=True | ||||
|     ) | ||||
|  | ||||
|   | ||||
| @@ -128,6 +128,13 @@ | ||||
|                     </div> | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|  | ||||
|                 <p> | ||||
|                     <!-- Details show/hide button --> | ||||
|                     <button id="toggle-part-details" class="btn btn-primary" data-toggle="collapse" data-target="#collapsible-part-details" value="show"> | ||||
|                     <span class="fas fa-chevron-down"></span> {% trans "Show Part Details" %} | ||||
|                     </button> | ||||
|                 </p> | ||||
|             </div> | ||||
|             </div> | ||||
|             <div class='info-messages'> | ||||
| @@ -208,13 +215,6 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <p> | ||||
|         <!-- Details show/hide button --> | ||||
|         <button id="toggle-part-details" class="btn btn-primary" data-toggle="collapse" data-target="#collapsible-part-details" value="show"> | ||||
|         <span class="fas fa-chevron-down"></span> {% trans "Show Part Details" %} | ||||
|         </button> | ||||
|     </p> | ||||
|          | ||||
|     <div class="collapse" id="collapsible-part-details"> | ||||
|         <div class="card card-body"> | ||||
|             <!-- Details Table --> | ||||
|   | ||||
| @@ -148,7 +148,6 @@ class StockItemSerializer(InvenTreeModelSerializer): | ||||
|  | ||||
|     purchase_price = InvenTreeMoneySerializer( | ||||
|         label=_('Purchase Price'), | ||||
|         max_digits=19, decimal_places=4, | ||||
|         allow_null=True | ||||
|     ) | ||||
|  | ||||
|   | ||||
| @@ -283,6 +283,11 @@ function setupFilterList(tableKey, table, target) { | ||||
|  | ||||
|     element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-default filter-tag'><span class='fas fa-redo-alt'></span></button>`); | ||||
|  | ||||
|     // Callback for reloading the table | ||||
|     element.find(`#reload-${tableKey}`).click(function() { | ||||
|         $(table).bootstrapTable('refresh'); | ||||
|     }); | ||||
|  | ||||
|     // If there are no filters defined for this table, exit now | ||||
|     if (jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) { | ||||
|         return; | ||||
| @@ -303,11 +308,6 @@ function setupFilterList(tableKey, table, target) { | ||||
|         element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`); | ||||
|     } | ||||
|  | ||||
|     // Callback for reloading the table | ||||
|     element.find(`#reload-${tableKey}`).click(function() { | ||||
|         $(table).bootstrapTable('refresh'); | ||||
|     }); | ||||
|  | ||||
|     // Add a callback for adding a new filter | ||||
|     element.find(`#${add}`).click(function clicked() { | ||||
|  | ||||
|   | ||||
| @@ -621,6 +621,10 @@ function submitFormData(fields, options) { | ||||
|  | ||||
|     var has_files = false; | ||||
|  | ||||
|     var data_valid = true; | ||||
|  | ||||
|     var data_errors = {}; | ||||
|  | ||||
|     // Extract values for each field | ||||
|     for (var idx = 0; idx < options.field_names.length; idx++) { | ||||
|  | ||||
| @@ -633,6 +637,21 @@ function submitFormData(fields, options) { | ||||
|  | ||||
|         if (field) { | ||||
|  | ||||
|             switch (field.type) { | ||||
|             // Ensure numerical fields are "valid" | ||||
|             case 'integer': | ||||
|             case 'float': | ||||
|             case 'decimal': | ||||
|                 if (!validateFormField(name, options)) { | ||||
|                     data_valid = false; | ||||
|      | ||||
|                     data_errors[name] = ['{% trans "Enter a valid number" %}']; | ||||
|                 } | ||||
|                 break; | ||||
|             default: | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
|             var value = getFormFieldValue(name, field, options); | ||||
|  | ||||
|             // Handle file inputs | ||||
| @@ -662,6 +681,11 @@ function submitFormData(fields, options) { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (!data_valid) { | ||||
|         handleFormErrors(data_errors, fields, options); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     var upload_func = inventreePut; | ||||
|  | ||||
|     if (has_files) { | ||||
| @@ -732,7 +756,8 @@ function updateFieldValues(fields, options) { | ||||
|  * Update the value of a named field | ||||
|  */ | ||||
| function updateFieldValue(name, value, field, options) { | ||||
|     var el = $(options.modal).find(`#id_${name}`); | ||||
|  | ||||
|     var el = getFormFieldElement(name, options); | ||||
|  | ||||
|     if (!el) { | ||||
|         console.log(`WARNING: updateFieldValue could not find field '${name}'`); | ||||
| @@ -760,6 +785,46 @@ function updateFieldValue(name, value, field, options) { | ||||
| } | ||||
|  | ||||
|  | ||||
| // Find the named field element in the modal DOM | ||||
| function getFormFieldElement(name, options) { | ||||
|  | ||||
|     var el = $(options.modal).find(`#id_${name}`); | ||||
|  | ||||
|     if (!el.exists) { | ||||
|         console.log(`ERROR: Could not find form element for field '${name}'`); | ||||
|     } | ||||
|  | ||||
|     return el; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Check that a "numerical" input field has a valid number in it. | ||||
|  * An invalid number is expunged at the client side by the getFormFieldValue() function, | ||||
|  * which means that an empty string '' is sent to the server if the number is not valud. | ||||
|  * This can result in confusing error messages displayed under the form field. | ||||
|  *  | ||||
|  * So, we can invalid numbers and display errors *before* the form is submitted! | ||||
|  */ | ||||
| function validateFormField(name, options) { | ||||
|  | ||||
|     if (getFormFieldElement(name, options)) { | ||||
|  | ||||
|         var el = document.getElementById(`id_${name}`); | ||||
|  | ||||
|         if (el.validity.valueMissing) { | ||||
|             // Accept empty strings (server will validate) | ||||
|             return true; | ||||
|         } else { | ||||
|             return el.validity.valid; | ||||
|         } | ||||
|     } else { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Extract and field value before sending back to the server | ||||
|  * | ||||
| @@ -771,7 +836,7 @@ function updateFieldValue(name, value, field, options) { | ||||
| function getFormFieldValue(name, field, options) { | ||||
|  | ||||
|     // Find the HTML element | ||||
|     var el = $(options.modal).find(`#id_${name}`); | ||||
|     var el = getFormFieldElement(name, options); | ||||
|  | ||||
|     if (!el) { | ||||
|         return null; | ||||
| @@ -1086,7 +1151,9 @@ function addFieldCallbacks(fields, options) { | ||||
|  | ||||
| function addFieldCallback(name, field, options) { | ||||
|  | ||||
|     $(options.modal).find(`#id_${name}`).change(function() { | ||||
|     var el = getFormFieldElement(name, options); | ||||
|  | ||||
|     el.change(function() { | ||||
|  | ||||
|         var value = getFormFieldValue(name, field, options); | ||||
|  | ||||
| @@ -1299,7 +1366,7 @@ function initializeRelatedField(field, fields, options) { | ||||
|     } | ||||
|  | ||||
|     // Find the select element and attach a select2 to it | ||||
|     var select = $(options.modal).find(`#id_${name}`); | ||||
|     var select = getFormFieldElement(name, options); | ||||
|  | ||||
|     // Add a button to launch a 'secondary' modal | ||||
|     if (field.secondary != null) { | ||||
| @@ -1492,7 +1559,7 @@ function initializeRelatedField(field, fields, options) { | ||||
|  */ | ||||
| function setRelatedFieldData(name, data, options) { | ||||
|  | ||||
|     var select = $(options.modal).find(`#id_${name}`); | ||||
|     var select = getFormFieldElement(name, options); | ||||
|  | ||||
|     var option = new Option(name, data.pk, true, true); | ||||
|  | ||||
| @@ -1513,9 +1580,7 @@ function setRelatedFieldData(name, data, options) { | ||||
|  | ||||
| function initializeChoiceField(field, fields, options) { | ||||
|  | ||||
|     var name = field.name; | ||||
|  | ||||
|     var select = $(options.modal).find(`#id_${name}`); | ||||
|     var select = getFormFieldElement(field.name, options); | ||||
|  | ||||
|     select.select2({ | ||||
|         dropdownAutoWidth: false, | ||||
| @@ -1926,8 +1991,17 @@ function constructInputOptions(name, classes, type, parameters) { | ||||
|         opts.push(`placeholder='${parameters.placeholder}'`); | ||||
|     } | ||||
|  | ||||
|     if (parameters.type == 'boolean') { | ||||
|     switch (parameters.type) { | ||||
|     case 'boolean': | ||||
|         opts.push(`style='display: inline-block; width: 20px; margin-right: 20px;'`); | ||||
|         break; | ||||
|     case 'integer': | ||||
|     case 'float': | ||||
|     case 'decimal': | ||||
|         opts.push(`step='any'`); | ||||
|         break; | ||||
|     default: | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     if (parameters.multiline) { | ||||
|   | ||||
| @@ -864,6 +864,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) { | ||||
|             }, | ||||
|             { | ||||
|                 sortable: true, | ||||
|                 switchable: false, | ||||
|                 field: 'quantity', | ||||
|                 title: '{% trans "Quantity" %}', | ||||
|                 footerFormatter: function(data) { | ||||
| @@ -879,18 +880,29 @@ function loadPurchaseOrderLineItemTable(table, options={}) { | ||||
|                 field: 'purchase_price', | ||||
|                 title: '{% trans "Unit Price" %}', | ||||
|                 formatter: function(value, row) { | ||||
|                     return row.purchase_price_string || row.purchase_price; | ||||
|                     var formatter = new Intl.NumberFormat( | ||||
|                         'en-US', | ||||
|                         { | ||||
|                             style: 'currency', | ||||
|                             currency: row.purchase_price_currency | ||||
|                         } | ||||
|                     ); | ||||
|                     return formatter.format(row.purchase_price); | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 field: 'total_price', | ||||
|                 sortable: true, | ||||
|                 field: 'total_price', | ||||
|                 title: '{% trans "Total price" %}', | ||||
|                 title: '{% trans "Total Price" %}', | ||||
|                 formatter: function(value, row) { | ||||
|                     var total = row.purchase_price * row.quantity; | ||||
|                     var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency}); | ||||
|                     return formatter.format(total); | ||||
|                     var formatter = new Intl.NumberFormat( | ||||
|                         'en-US', | ||||
|                         { | ||||
|                             style: 'currency', | ||||
|                             currency: row.purchase_price_currency | ||||
|                         } | ||||
|                     ); | ||||
|                     return formatter.format(row.purchase_price * row.quantity); | ||||
|                 }, | ||||
|                 footerFormatter: function(data) { | ||||
|                     var total = data.map(function(row) { | ||||
| @@ -1436,7 +1448,7 @@ function loadSalesOrderLineItemTable(table, options={}) { | ||||
|             sortable: true, | ||||
|             field: 'reference', | ||||
|             title: '{% trans "Reference" %}', | ||||
|             switchable: false, | ||||
|             switchable: true, | ||||
|         }, | ||||
|         { | ||||
|             sortable: true, | ||||
| @@ -1456,14 +1468,6 @@ function loadSalesOrderLineItemTable(table, options={}) { | ||||
|             field: 'sale_price', | ||||
|             title: '{% trans "Unit Price" %}', | ||||
|             formatter: function(value, row) { | ||||
|                 return row.sale_price_string || row.sale_price; | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             sortable: true, | ||||
|             title: '{% trans "Total price" %}', | ||||
|             formatter: function(value, row) { | ||||
|                 var total = row.sale_price * row.quantity; | ||||
|                 var formatter = new Intl.NumberFormat( | ||||
|                     'en-US', | ||||
|                     { | ||||
| @@ -1472,7 +1476,23 @@ function loadSalesOrderLineItemTable(table, options={}) { | ||||
|                     } | ||||
|                 ); | ||||
|  | ||||
|                 return formatter.format(total); | ||||
|                 return formatter.format(row.sale_price); | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             field: 'total_price', | ||||
|             sortable: true, | ||||
|             title: '{% trans "Total Price" %}', | ||||
|             formatter: function(value, row) { | ||||
|                 var formatter = new Intl.NumberFormat( | ||||
|                     'en-US', | ||||
|                     { | ||||
|                         style: 'currency', | ||||
|                         currency: row.sale_price_currency | ||||
|                     } | ||||
|                 ); | ||||
|  | ||||
|                 return formatter.format(row.sale_price * row.quantity); | ||||
|             }, | ||||
|             footerFormatter: function(data) { | ||||
|                 var total = data.map(function(row) { | ||||
| @@ -1544,6 +1564,7 @@ function loadSalesOrderLineItemTable(table, options={}) { | ||||
|     if (pending) { | ||||
|         columns.push({ | ||||
|             field: 'buttons', | ||||
|             switchable: false, | ||||
|             formatter: function(value, row, index, field) { | ||||
|  | ||||
|                 var html = `<div class='btn-group float-right' role='group'>`; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user